3 函数
3.5 闭包(closures)
3.5.1 作用域链
与很多程序设计语言不同,javascript不存在大括号级的作用域,但它有函数作用域,即在函数内定义的变量在函数外是不可见的。但如果该变量是在某个代码块中定义的(如在某个if或for语句中),它在代码块外是可见的。
>>>var a=1;function f(){var b=1;return a;}
>>>f();
1
>>>b
b is not defined
变量a是属于全局域的,而变量b的作用域就在函数f()内了。所以
- 在f()内,a和b都是可见的;
- 在f()外,a是可见的,b则不可见。
示例:
var a=1;
function f(){
var b=1;
function n(){
var c=3;
}
}
如果我们在函数f()中定义了另一函数n(),那么,在n()中可以访问的变量既可以来自它自身的作用域,也可以来自其“父级”作用域。这就形成了一条作用域链(scope chain),该链的长度(或深度)去取决于我们需要。
3.5.2 词法作用域
在javascript中,每个函数都有一个自己的词法作用域。也就是说,每个函数在被定义时(而非执行时)都会创建一个属于自己的环境(即作用域)。
示例:
>>>function f1(){var a=1;f2();}
>>>function f2(){return a;}
>>>f1();
a is not defined
错误想法:由于局部变量a也在f1()中,所以f2()是可以访问a的。
原因:因为当f2()被定义时(不是执行时),变量a是不可见的。和f1()一样,它那时候只能访问自身作用域和全局作用域中的内容。也就是说,这里的f1()、f2()之间不存在共享的词法作用域。
>>>var a=5;
>>>f1();
5
>>>a=55;
>>>f1();
55
>>>delete a;
true
>>>f1();
a is not defined
我们可以在函数中对变量执行添加、移除和更新等操作,但函数只会看到该变量的最终状态。
如上,再增加一个全局变量a,f2()就可以访问它了,因为f2()知道访问全局环境的途径,它可以访问该环境的所有东西。另外要注意的是,即使f2()还没有被定义,我们也可以在f1()的定义中包含对f2()的调用。因为对于f1()而言,在其所知的作用域中的任何东西都是可用的。
可通过添加/删除操作来实现变量的重复添加,并且完全不会影响程序的运行。
在上例中可以删除f2函数,并重新给它定义一个完全不同的执行体,而最终f1()依然正常工作。因为在这里,f1()只需知道它应该如何访问自身作用域即可,不关心该作用域在什么时候发生了什么事。例如:
>>>delete f2;
true
>>>f1()
f2 is not defined
>>>var f2=function(){return a*2;}
>>>var a=5
5
>>>f1();
10
3.5.3 利用闭包突破作用域链
前提: 闭包的概念
可访问空间 | G | F | N |
---|---|---|---|
a | T | F | F |
b | T | T | F |
c | T | T | T |
全局作用域G包含各种变量(如a)和函数(如F),每个函数也都会拥有一块属于自己的私用空间,用以存储一些别的变量(如b)和函数(如N)。上图中,a和b之间是不连通的,因为b在F以外是不可见的。但可将c点和b点连通起来,或者说将N与b连通起来。当我们将N的空间扩展到F以外,并止步于全局空间以内时,就产生了闭包。
N将会和a一样置身于全局空间,且由于函数还记得它在被定义时所设定的环境,因此它依然可以访问F空间并使用b。有趣的是,因为现在N和a同处于一个空间,但N可以访问b,而a不能。
N如何突破作用域链?只需将它们升级为全局变量(不使用var语句)或通过F传递(或返回)给全局空间即可。具体做法:
3.5.3.1 闭包#1
function f(){
var b="b";
return function(){
return b;
}
}
>>>b
b is not defined
f()的返回值,可看成上图中的n,b对其可见。由于f()是一个全局函数,所以可以将其返回值赋值给另一个全局变量,从而生成一个可以访问f()私有空间的新全局函数。
>>>var n=f();
>>>n();
"b"
3.5.3.2 闭包#2
下例结果与闭包#1相同,实现方法上这里f()不再返回函数,而是直接在函数体内创建一个新的全局函数。
首先需要声明一个全局函数的占位符(最好声明,尽管这种占位符非必需),然后将函数f()定义如下:
var n;
function f(){
var b="b";
n=function(){
return b;
}
}
>>>f();
>>>n();
"b"
在f()中定义一个新函数,并且没有在这里使用var语句,因此它应该属于全局的,由于n()是在f()内部定义的,它可以访问f()的作用域,所以即使该函数后来升级成了全局函数,但它依然可以保留对f()作用域的访问权。
3.5.3.3 相关定义与闭包#3
如果一个函数需要在其父级函数返回之后留住对父级作用域的链接的话,就必须要为此建立一个闭包。(f是n的父级函数,在f返回之后,n依然可以访问f中的局部变量b)
而由于函数通常都会将自身的参数视为局部变量。因此创建返回函数时,也可以令其返回父级函数的参数。例如:
function f(arg){
var n=function(){
return arg;
};
arg++;
return n;
}
>>> var m=f(123);
>>>m();
124
注意:当返回函数被调用时(n被赋值时函数并没有被调用,调用是在n被求值,也就是执行return n;语句时被调用的),arg++已经执行过一次递增操作了,所以m()返回的是更新后的值。由此看出,函数所绑定的是作用域本身,而不是该作用域中的变量或变量当前所返回的值。
3.5.3.4 循环中的闭包
function f(){
var a=[];
var i;
for(i=0;i<3;i++){
a[i]=function(){
return i;
}
}
return a;
}
>>>var a=f();
>>>a[0]();
3
>>>a[1]();
3
>>>a[2]();
3
希望输出是1 2 3,由于创建了三个闭包,它们都指向了一个共同的局部变量i。但是闭包不会记录它们的值,它们所拥有的只是一个i的连接(即引用),因此只能返回i的当前值。由于循环结束时i的值为3,所以这三个函数都指向了这一共同值。
纠正:显然需要三个不同的变量,换一种闭包形式:
function f(){
var a=[];
var i;
for(i=0;i<3;i++){
a[i]=(function(x){
return function(){
return x;
}
})(i);
}
return a;
}
在此不再直接创建一个返回i的函数,而是将i传递给了一个自调函数。在该函数中,i就被赋值给了局部变量x,这样使每次迭代中的x拥有各自不同的值。
或者,可以定义一个“正常点”(不使用自调函数)的内部函数实现相同功能。关键:在每次迭代操作中,在中间函数内将i的值“本地化”。
function f(){
function makeClosure(x){
return function(){
return x
}
}
var a=[];
var i;
for(i=0;i<3;i++){
a[i]=makeClosure(x);
}
return a;
}
3.5.4 Getter与Setter
var getValue,setValue;
(function(){
var secret=0;
getValue=function(){
return secret;
}
setValue=function(v){
secret=v;
}
})();
使用两个函数来确保局部变量secret的不可直接访问性。
>>>getValue()
0
>>>setValue(123)
>>>getValue()
123
3.5.5 迭代器
有些复杂的数据结构,通常会有着与数组截然不同的序列规则,这需要将一些“谁是下一个”的复杂逻辑封装成易于使用的next()函数,然后只需简单调用next()就能实现对于相关的遍历操作。
下例接受数组输入的初始化函数,定义了一个私有指针,该指针始终指向数组中的下一个元素。
function step(x){
var i=0;
return function(){
return x[i++];
};
}
>>>var next=setup(['a','b','c']);
>>>next();
"a"
>>>next();
"b"
>>>next();
"c"