7.闭包
作用域链
JavaScript与很多程序设计语言不同,它不存在大括号级的作用域,但它有函数作用域。也就是说,在函数内定义的变量在函数外是不可见的。
var a=1; function f(){ var b=1; return a; }
在这里,变量a是属于全局域的,而变量b的作用域就在函数f()内了。所以:
- 在f()内,a和b都是可见的。
- 在f()外,a是可见的,b则不可见。
另外,如果我们在函数f()中定义了另一个函数n(),那么,在n()中可以访问的变量可以来自它自身的作用域,也可以来自其”父级“作用域。这就形成了一条作用域链,该链的长度(或深度)则取决于我们的需要。
var a=1; function f(){ var b=1; function n(){ var c=3; } }
词法作用域
在JavaScript中,每个函数都有一个自己的词法作用域。也就是说,每个函数在被定义是(而非执行时)都会创建一个属于自己的环境(即作用域)。
function f1(){ var a = 1; f2(); } function f2(){ return a;//a is not defined } f1();
上面的代码中,我们在函数f1()中调用了函数f2()。由于局部变量a也在f1()中,所以人们可能认为f2()是可以访问a的,但是事实并非如此。因为f2()被定义时(不是执行时),变量a是不可见的。也就是说,这里的f1()、f2()之间不存在共享的词法作用域。
利用闭包突破作用域链
首先是全局作用域,我们可以将其看做包含一切的宇宙(G),其中也可以包含各种变量(a)和函数(F)。每个函数也都会拥有一块属于自己的私有空间,可以存储一些别的变量和函数。所有,我们可以把示意图画成这样:
上图中,如果我们在a点,那么就位于全局空间中。而如果是在b点,我们就在函数F的空间中,在这里我们既可以访问全局空间,也可以访问F空间。如果我们在c点,那就位于函数N中,这里我们可以访问的空间包括全局空间、F空间和N空间。其中,a和b之间是不连通的,因为b在F以外是不可见的。但如果愿意的话,我们是可以将c点和b点连通起来的,或者说将N与b连通起来。当我们将N的空间扩展到F以外,并止步于全局空间以内时,就产生了一件有趣的东西--闭包。
知道接下来会发生什么吗?N将会和a一样置身于全局空间。而且由于函数还记得它在被定义是所设定的环境,因此它依然可以访问F空间并使用b。这很有趣,因为现在N和a同处于一个空间,但N可以访问b,而a不能。
那么,N究竟是如何突破作用域链的呢?我们只需要将它们升级为全局变量或通过F传递(或返回)给全局空间即可。下面,我们来看看具体怎么做。
闭包#1
下面,我们先来看一个函数:
function f(){ var b = "b"; return function(){ return b; } }
这个函数含有一个局部变量b,它在全局空间里是不可见的。接下来,我们看一下f()的返回值。因为f()可以在全局空间中被调用(它是一个全局函数),所以我们可以将它的返回值复制给另一个全局变量,从而生成一个可以访问的f()私有空间的新全局函数。
闭包#2
下面这个例子的最终结果与之前相同,但在实现方法上存在着一些细微的不同。
由于n()是在f()内部定义的,它可以访问f()的作用域,所有即使该函数后来升级为了全局函数,但它依然可以保留对f()作用域的访问权。
闭包#3
如上例所示,f是n的父级函数,在f返回之后,n依然可以访问f中的局部变量b。定义:如果一个函数需要在其父级函数返回后留住对其父级作用域的链接的话,就必须要为此建立一个闭包。
循环中的闭包#4
让我们来一个三次性的循环操作,它在每次迭代中都会创建一个返回当前循环序号的新函数。该新函数会被添加到一个数组中,并最终返回。
function f(){ var a = []; var i; for(i = 0;i<3;i++){ a[i]=function(){ return i; } } return a; }
按通常的估计,它们应该会依照循环顺序分别输出0、1和2。下面是运行结果:
事实上不是这样的。这是怎么回事?原来屋面在这里创建三个闭包,它们都执行了一个共同的局部变量i。但是,闭包并不记录它们的值,它们所拥有的是一个i的引用,因此只能返回i当前值。由于循环结束时i的值为3,所以这三个函数都指向了这一共同值。
那么,应该如何纠正这种行为呢?在这里,我们不再直接创建一个返回i的函数了,而是将i传递给了一个自调函数。在该函数中,i就被赋值给了局部变量x,这样一来,每次迭代中的x就会拥有各自不同的值了。
function f(){ var a = []; var i; for(){ a[i] = (function(x){return function(){return x;}})(i); } retrun a; }
Getter与Setter
假如现在有一个属于特殊区间的变量,我们不想将它暴露给外部。因为这样一来,其他部分的代码就有修改它的可能,所有我们需要将它保护在相关函数的内部,然后再提供两个额外的函数——一个用于访问变量,另一个用设置变量。
我们需要将getter和setter这两个函数放在一个共同的函数中,并在该函数中定义secret变量,这使得两个函数能够共享一个作用域。
var getValue,setValue; ( function(){ var secret=0; getValue=function(){ return secret; } setValue=function(v){ secret=v; } } )();
所有一切都是通过一个匿名自调函数来实现的,我们在其中定义了全局函数setValue()和getValue(),并以此来确保局部变量secret的不可直接访问。
迭代器
通常情况下,我们都知道如何用循环来遍历一个简单的数组,但是有时候我们需要面对更复杂的数据结构,它们通常会有着与数组截然不同的序列规则。这个时候需要将一些“谁是下一个”的复杂逻辑封装成易于使用的next()函数。然后,我们只需要简单地调用next()就能实现相关的遍历操作了。
下面是一个接受数组输入的初始化函数,我们在其中定义了一个私有指针,该指针会始终指向数组中的下一个元素。
function setup(x){ var i = 0; return function(){ return x[i++]; } }
现在,我们只需用一组数据来调用一下setup(),就会创建出我们所需要的next()函数: