js原型链闭包作用域链-Tom

时间:2021-10-25 16:05:30

1、原型相当于Java、C++里面的父类,由封装公有属性及方法而产生,子类可以继承。

原型继承实现(函数的原型属性指向原型函数一个实例对象,函数的原型的构造函数指向函数本身)

1)eg:原型链

 function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
}; function Bar() {} // 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World'; // 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar; var test = new Bar() // 创建Bar的一个新实例 // 原型链
test [Bar的实例]
Bar.prototype [Foo的实例]
{ foo: 'Hello World' }
Foo.prototype
{method: ...};
Object.prototype
{toString: ... /* etc. */}; 上面的例子中,test 对象从 Bar.prototype 和 Foo.prototype 继承下来;因此,它能访问 Foo 的原型方法 method。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value。
需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是重复使用它原型上的那个实例;因此,所有的 Bar 实例都会共享相同的 value 属性。

2)原型使用方法:

原型使用方式1:

在使用原型之前,我们需要先将代码做一下小修改:

        var Calculator = function (decimalDigits, tax) {
this.decimalDigits = decimalDigits;
this.tax = tax;
};

然后,通过给Calculator对象的prototype属性赋值对象字面量来设定Calculator对象的原型。

        Calculator.prototype = {
add: function (x, y) {
return x + y;
}, subtract: function (x, y) {
return x - y;
}
};
//alert((new Calculator()).add(1, 3));

这样,我们就可以new Calculator对象以后,就可以调用add方法来计算结果了。

原型使用方式2:

第二种方式是,在赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式:

Calculator.prototype = function () { } ();

它的好处在前面的帖子里已经知道了,就是可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果,修改后的代码如下:

 Calculator.prototype = function () {
add = function (x, y) {
return x + y;
}, subtract = function (x, y) {
return x - y;
}
return {
add: add,
subtract: subtract
}
} (); //alert((new Calculator()).add(11, 3));

同样的方式,我们可以new Calculator对象以后调用add方法来计算结果了。

3)不让函数访问原函数构造函数里面的属性:

 1 var BaseCalculator = function() {
2 this.decimalDigits = 2;
3 };
4
5 BaseCalculator.prototype = {
6 add: function(x, y) {
7 return x + y;
8 },
9 subtract: function(x, y) {
10 return x - y;
11 }
12 };
var Calculator = function () {
//为每个实例都声明一个税收数字
this.tax = 5;
}; Calculator.prototype = new BaseCalculator();

如果我不想让Calculator访问BaseCalculator的构造函数里声明的属性值,那怎么办呢?这么办:

 1 var Calculator = function () {
2 this.tax= 5;
3 };
4
5 Calculator.prototype = BaseCalculator.prototype;
6
7 通过将BaseCalculator的原型赋给Calculator的原型,这样你在Calculator的实例上就访问不到那个decimalDigits值了,如果你访问如下代码,那将会提升出错。
8
9 var calc = new Calculator();
10 alert(calc.add(1, 1));
11 alert(calc.decimalDigits);

2、作用域链

作用域链(Scope Chains)

A scope chain is a list of objects that are searched for identifiers appear in the code of the context.
作用域链是一个 对象列表(list of objects) ,用以检索上下文代码中出现的 标识符(identifiers) 。

作用域链的原理和原型链很类似,如果这个变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层。

标示符[Identifiers]可以理解为变量名称、函数声明和普通参数。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为*变量[free variable]。那么我们搜寻这些*变量就需要用到作用域链。

在一般情况下,一个作用域链包括父级变量对象(variable object)(作用域链的顶部)、函数自身变量VO和活动对象(activation object)。不过,有些情况下也会包含其它的对象,例如在执行期间,动态加入作用域链中的—例如with或者catch语句。[译注:with-objects指的是with语句,产生的临时作用域对象;catch-clauses指的是catch从句,如catch(e),这会产生异常对象,导致作用域变更]。

当查找标识符的时候,会从作用域链的活动对象部分开始查找,然后(如果标识符没有在活动对象中找到)查找作用域链的顶部,循环往复,就像作用域链那样。

var x = 10;

(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x"和"y"是*变量
// 会在作用域链的下一个对象中找到(函数”bar”的互动对象之后)
console.log(x + y + z);
})();
})();

我们假设作用域链的对象联动是通过一个叫做__parent__的属性,它是指向作用域链的下一个对象。这可以在Rhino Code中测试一下这种流程,这种技术也确实在ES5环境中实现了(有一个称为outer链接).当然也可以用一个简单的数据来模拟这个模型。使用__parent__的概念,我们可以把上面的代码演示成如下的情况。(因此,父级变量是被存在函数的[[Scope]]属性中的)。

js原型链闭包作用域链-Tom

图 9. 作用域链

在代码执行过程中,如果使用with或者catch语句就会改变作用域链。而这些对象都是一些简单对象,他们也会有原型链。这样的话,作用域链会从两个维度来搜寻。

  1. 首先在原本的作用域链
  2. 每一个链接点的作用域的链(如果这个链接点是有prototype的话)

我们再看下面这个例子:

Object.prototype.x = 10;

var w = 20;
var y = 30; // 在SpiderMonkey全局对象里
// 例如,全局上下文的变量对象是从"Object.prototype"继承到的
// 所以我们可以得到“没有声明的全局变量”
// 因为可以从原型链中获取 console.log(x); // 10 (function foo() { // "foo" 是局部变量
var w = 40;
var x = 100; // "x" 可以从"Object.prototype"得到,注意值是10哦
// 因为{z: 50}是从它那里继承的 with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
} // 在"with"对象从作用域链删除之后
// x又可以从foo的上下文中得到了,注意这次值又回到了100哦
// "w" 也是局部变量
console.log(x, w); // 100, 40 // 在浏览器里
// 我们可以通过如下语句来得到全局的w值
console.log(window.w); // 20 })();

我们就会有如下结构图示。这表示,在我们去搜寻__parent__之前,首先会去__proto__的链接中。

js原型链闭包作用域链-Tom

图 10. with增大的作用域链

注意,不是所有的全局对象都是由Object.prototype继承而来的。上述图示的情况可以在SpiderMonkey中测试。

只要所有外部函数的变量对象都存在,那么从内部函数引用外部数据则没有特别之处——我们只要遍历作用域链表,查找所需变量。然而,如上文所提及,当一个上下文终止之后,其状态与自身将会被 销毁(destroyed) ,同时内部函数将会从外部函数中返回。此外,这个返回的函数之后可能会在其他的上下文中被激活,那么如果一个之前被终止的含有一些*变量的上下文又被激活将会怎样?通常来说,解决这个问题的概念在ECMAScript中与作用域链直接相关,被称为 (词法)闭包((lexical) closure)。

闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域。通过这些保存的作用域来搜寻到函数中的*变量。

3、闭包(总结:闭包即为调用外部变量的内部函数,所有函数都是闭包)

根据函数创建的算法,我们看到 在ECMAScript中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 —— [[Scope]]在函数创建的时候就有了):

这里还是有必要再次强调下:ECMAScript只使用静态(词法)作用域(而诸如Perl这样的语言,既可以使用静态作用域也可以使用动态作用域进行变量声明)。

 var x = 10;

 function foo() {
alert(x);
} (function (funArg) { var x = 20; // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了
funArg(); // 10, 而不是20 })(foo);
再说一下,因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包)。

这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。

为了更好的澄清该问题,我们对ECMAScript中的闭包给出2个正确的版本定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问*变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了*变量

闭包用法实战

实际使用的时候,闭包可以创建出非常优雅的设计,允许对funarg上定义的多种计算方式进行定制。如下就是数组排序的例子,它接受一个排序条件函数作为参数:

[1, 2, 3].sort(function (a, b) {
... // 排序条件
});

同样的例子还有,数组的map方法是根据函数中定义的条件将原数组映射到一个新的数组中:

[1, 2, 3].map(function (element) {
return element * 2;
}); // [2, 4, 6]

使用函数式参数,可以很方便的实现一个搜索方法,并且可以支持无限制的搜索条件:

someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
});

还有应用函数,比如常见的forEach方法,将函数应用到每个数组元素:

[1, 2, 3].forEach(function (element) {
if (element % 2 != 0) {
alert(element);
}
}); // 1, 3

顺便提下,函数对象的 apply 和 call方法,在函数式编程中也可以用作应用函数。 apply和call已经在讨论“this”的时候介绍过了;这里,我们将它们看作是应用函数 —— 应用到参数中的函数(在apply中是参数列表,在call中是独立的参数):

(function () {
alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

闭包还有另外一个非常重要的应用 —— 延迟调用:

var a = 10;
setTimeout(function () {
alert(a); // 10, after one second
}, 1000);

还有回调函数

//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
// 当数据就绪的时候,才会调用;
// 这里,不论是在哪个上下文中创建
// 此时变量“x”的值已经存在了
alert(x); // 10
};
//...

还可以创建封装的作用域来隐藏辅助对象:


 var foo = {};
// 初始化(function (object) {
var x = 10;
object.getX = function _getX() { return x; };
})(foo);
alert(foo.getX()); // 获得闭包 "x" – 10