闭包是Javascript常说的一个话题,面试官都问的问题,很多高级应用都要依靠闭包实现。

1
闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境。

要理解闭包,首先要弄懂Javascript的变量作用域。

变量的作用域

这是一个至关重要的问题。变量的定义并不是以代码块作为域的,而是以函数作为作用域。也就是说,如果变量是在某个函数中定义的,那么它的函数以外的地方是不可见的。而如果变量是定义在if或者for这样的代码块中的,它的代码块之外是可见的。

  • 全局变量:指的是定义在全局代码中的变量。

  • 局部变量:指的在函数里定义的变量。

其中,函数内的代码可以像访问自己的局部变量那样访问全局变量,反之则不行。

如果我们声明一个变量时没有使用var语句,该变量就会黙认为全局变量

尽量将全局变量的数量降到最低,以避免命名冲突。想想,如果两个人在同一段脚本的不同函数中使用了相同的全局变量名,就会发生令你捉狂的BUG,也难以找到。

  • 最好使用var来声时变量(ps:es6中,let比var更完美,实现块级作用域)。

  • 最好可以在函数的第一行,定义变量,后期也轻松找到相关变量的定义。

变量提升

1
2
3
4
5
6
7
var a = 123;
function f(){
alert(a);
var a = 1;
alert(a);
}
f();

这里第一个alert()是undefined,这是因为函数域始终优先于全局域,所以局部变量a会覆盖所有与它同名的全局变量,尽管第一次调用时,a还没正式定义(值为undefined),但a已经存在于本地空间了。这就是提升

当Javascript执行过程进入新的函数时,这个函数内被声明的所有变量都会被提升到函数最开始的地方。

注意是,提升的只是变量,相关的赋值操作并不会被提升。

突破作用域链

上图左边,如果在C点,那就是位于函数N中,是可以访问全局空间、F空间和N空间。其中,a和b是不连通的,因为b在F以外是不可见的。当将N空间扩展到F以外(上图右边),并止步于全局空间以内时,就产生个厉害的东西——闭包。

N和a一样,是在全局空间,而且由于函数还记得它在被定时所设定的环境,因此它依然可以访问F空间并使用b。

相关定义与闭包

事实上,每个函数都可以被认为是一个闭包。因为每个函数都在其所在作用域中维护了某种私有联系。但在大多数时候,该作用域在函数执行完就自行销毁了。

像前面说的突破的作用域,导致作用域被保持。

循环中的闭包

先来看来经典例子:

1
2
3
4
5
6
7
8
9
function F(){
var arr = [];
for(var i=0; i<3; i++){
arr[i] = function(){
return i;
}
}
return arr;
}

运行后,结果是arr=[3,3,3]

显然,这并不是我想要的结果。

原来这里创建了三个闭包,而它们都指向了一个共同的局部变量i。但是,闭包并不会记录它们的值,它们所拥有的只是相关域在创建时的一个连接(即引用)。在这个例子中,变量i恰巧存在于定义这三个函数域中。对这个三个函数中的任保一个而言,当它要去获取某个变量时,它会从其所在的域开始逐级寻找那个距离最近的值。由于循环结时,i的值为3,所以这三个函数都提向了这一共同值。

想要预期的结果,就要换一种闭包形式:

1
2
3
4
5
6
7
8
9
10
11
function F(){
var arr = [];
for(var i=0; i<3; i++){
arr[i]=(function(x){
return function(){
return x
}
}(i))
}
return arr;
}

这样就能获得人们预期的结果了arr=[0,1,2]