浅谈闭包

一、现状

闭包是jser绕不过的坎,一直在都在说,套用 simpson 的话来说:JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

闭包是基于词法作用域书写代码时的自然结果,你甚至不需要为了利用它们而有意识的去创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的只是根据你的意愿来识别、拥抱和影响闭包的思维环境

二、什么是闭包(closure)

当函数可以记住并访问所在的词法作用域时,就产生了闭包。即使函数是在当前词法作用域之外执行 –《你不知道的js》(上卷)

闭包是指有权访问另一个函数作用域中的变量的函数 –《JavaScript高级程序设计》

先来看一个例子:
例子1:

1
2
3
4
5
6
7
8
9
10
function foo(){
var a = 2;

function bar(){
console.log(a); // 2
}
bar();
}

foo()

这是闭包吗?
这个代码从技术上来说是,但也可以说不是。准确的来说bar()对a的引用的方法是词法作用域的查找规则。我们再来看:
例子2:

1
2
3
4
5
6
7
8
9
10
11
function foo(){
var a = 2;

function bar(){
console.log(a)
}

return bar;
}
var baz = foo();
baz(); // 2, 这就是闭包了

在例2中,我们将bar()函数本身当做一个值类型进行传递,函数bar()能够访问foo()的内部作用域。在这个例子中,它在自己定义的词法作用域以外的地方执行。

三、怎么形成的

要了解清楚,得先了解几个概念

  • 作用域链(scope chain)
  • 词法作用域

词法作用域

每个函数都有自己的执行环境。这个环境可以访问外部环境,以此类推。每个环境能访问到的标识符集合,称之为 作用域,也就是词法作用域

作用域链(scope chain)

将作用域一层一层的嵌套,就形成了作用域链

如下,通常我们都希望 foo() 在执行完成以后,整个的内部作用域都被销毁。因为我们知道引擎有垃圾回收机制用来释放不再使用的内存空间。由于看上去 foo()的内容不会再被使用,所以很自然的想到会对其回收。但是,事实上内部作用域依然存在

1
2
3
4
5
6
7
8
9
10
11
var globalVar = 10;
function foo() {
var fooVar = 20;
function bar() {
var barVar = 30;
return globalVar + fooVar + barVar;
}
return bar;
}
var baz = foo();
baz();

如上,用一张图表示
作用域链

这个作用域链在函数创建的时候就保存起来了。

baz()函数在执行的时候(执行bar函数),将当前的变量对象(由于当前的环境是函数,所以将其活动对象作为变量对象)添加到作用域链的前端。此时,由于bar在执行,而作用域链也存在,所以可以在作用域链上进行查找,去访问foo的变量。

四、闭包的应用场景有哪些

  • 创建私有变量或函数

五、闭包的缺点

  • 闭包中的值是存在于内存中,滥用的话会导致内存消耗过大

闭包经典问题

1
2
3
4
5
6
7
8
9
10
// 函数作用:希望它返回一个数组。该数组的元素为遍历的索引值
function hello(){
var res = [];
for (var i = 0,len = 5;i < len;i++){
res[i] = function () {
return i;
}
}
return res;
}

返回的结果跟我们期待的不一样,因为:闭包保存的是整个变量对象,而不是每个变量。
解决方案:

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

这里,没有没有把闭包直接赋值给数组。而是定义了一个匿名函数,并且将立即执行该匿名函数的结果赋值给数组,由于参数是按值传递的,所以会将当前值传给参数num。

参考资料:《你不知道的js》(中卷)、《JavaScript高级程序设计》