对闭包的理解

闭包的概念

  初学闭包的时候,似懂非懂,后来看了《javascript高级程序设计》,里面对闭包的描述是

闭包是指有权访问另一个函数作用域中的变量的的函数。

  在这段描述里,把闭包理解为一个函数。阮一峰大神给出了类似的理解:

闭包就是能够读取其他函数内部变量的函数。

  可以看出是有很多相似之处的。再后来,知乎有了针对阮一峰对闭包概念描述准确性的论战,粗略看完众多回答后,我又去特地查询了MDN的文档

闭包是函数和声明该函数的词法环境的组合。

  emmmm。。。不明觉厉但是觉得可能是要更准确一些。感谢同桌 @于雅楠 买了一套《你不知道的javascript》,今天看到上面关于闭包部分的描述,感觉多了一些理解。

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

  这套书从作用域和编译器以及引擎的角度分析javascript的语法以及作用原理,相比《javascript高级程序设计》更底层一些,并且觉得学习底层机制是学习一门编程语言绕不开的问题。借用书中的例子。

1
2
3
4
5
6
7
8
9
function foo () {
var a = 2;
function bar (){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2

  一般来说,foo函数的作用域内的变量外部是访问不到的,而bar函数能访问foo函数体内的全部作用域,当foo函数返回bar函数时,也同时向外界提供了一个访问自己内部数据的“桥梁”,于是一个闭包产生了 —— bar函数记住了声明bar函数时的词法作用域(foo函数内部的作用域),并访问了所在的词法作用域(bar函数里引用了foo函数内bar函数外声明的变量a),即使函数是在当前词法作用域之外执行(bar函数在foo函数外执行)。

  当一个函数执行后,通常会被javascript的垃圾回收机制回收占用的资源——其创建的作用域将被销毁,而闭包阻止了回收的发生,由于bar维持着对foo函数作用域的引用,因而引擎始终认为这是一个活动着的作用域。

bar()依然持有对该作用域的引用,该引用就叫做闭包。这个函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。

无论通过何种手段把内部函数 传递 到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

  我自作主张的将以上的论述拆开来看。首先要有一个需要被引用的作用域,在这个作用域内又声明了一个内部函数,且这个内部函数持有被引用的作用域内的变量,然后将这个内部函数作为返回值传递到被引用作用域的外界。那么当在调用这个内部函数时,也就获得了对内部函数所引用的作用域的访问能力。在这个过程中,内部函数起到了桥梁或者说通道的作用,不仅避免了作用域被回收,也提供了访问作用域内变量的方法。

闭包的使用场景

  据说。闭包很常见。

延时函数造成的闭包

1
2
3
4
5
6
function wait (msg) {
setTimeout(function timer () {
console.log(msg);
}, 1000);
}
wait('emm..')

  javascript是个单线程语言,借此讨论一下它对setTimeout的处理。引用MDN文档中的论述。

setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字在非严格模式会指向 window (或全局)对象,这和所期望的this的值是不一样的。

  这段叙述是在解释setTimeout中执行一个函数时如果这个函数内含有this,那么这个this会指向window对象。也就是说,setTimeout函数会脱离当前函数的运行环境,转而在全局环境上执行传递给它的函数。

  在这个例子中,timer函数在获得wait函数作用域的引用后被setTimeout在全局环境中调用。所以内部函数timer在wait函数执行1000ms后仍保持着对wait作用域的引用,也就是造成了一个闭包。

循环中的闭包

  考虑一个很常见的说明闭包的例子。

1
2
3
4
5
for(var i = 0; i < 6; i++ ) {
setTimeout(function timer () {
console.log(i);
}, i*1000);
} // 6 *6

  我们可以看出这个循环的本意是在每次循环的时候都在控制台输出运行时的 i 值,然而输出的却是6个6。思考上面对setTimeout的解释,这个结果又是显而易见的 —— setTimeout使得timer函数的执行环境和for循环的执行环境脱离开来,尽管每次迭代都使用了setTimeout,但它们引用的是同一个变量 i 。因此timer函数输出了6个同样的 i 值。

  我们稍微改写一下上面的例子。

1
2
3
4
5
6
7
8
9
for(var i = 0; i < 6; i++ ){
setTimeout(timer(i),1000)
}
function timer (i) {
function inner() {
console.log(i);
}
return inner;
} // 012345

  timer函数在每次迭代时被调用,也同时创建了一个作用域,inner函数在这个作用域中被声明且返回,因此inner具有访问 i 的能力。inner中引用了 i ,相当于保存了当前循环中 i 的值。整个过程的实质是,每次迭代的时候timer函数都会创建一个不被销毁的作用域,而这个作用域中保存了当前 i 值的副本。于是当循环结束时,我们创建了6个作用域,每个作用域中都保存了一个 i ,所以这次获得了正确的输出。

1
2
3
4
5
6
7
for(var i = 0; i < 6; i++ ){	// 第一个循环
(function () {
setTimeout(function timer () {
console.log(i);
},1000)
})();
} // 6 *6

  这次,利用立即执行函数会即时创建一个作用域的特性再次改写上面的代码。显然,匿名的执行函数会在每次迭代的时候创建一个作用域,并在这个作用域中使用setTimeout调用timer函数,于是好像在每次创建作用域的时候都可以得到一个当前 i 值的副本,原理同上。然而实际输出的结果却是6个6。

  具体原因,再次分析这个立即执行函数,可以看到,setTimeout函数在全局执行,而立即执行函数中除了setTimeout再没有别的变量或者函数,因此这是一个“空的作用域”,为了能在这个作用域中保存 i 的值,需要再做一些小小的改动。

1
2
3
4
5
6
7
8
for(var i = 0; i < 6; i++ ){	// 第二个循环
(function () {
var j = i;
setTimeout(function timer () {
console.log(j);
},1000)
})();
} // 012345

  结果正确,改动只是在立即执行函数的作用域中增加了一个变量。

  总结至此,大概已经说完了目前我能了解的部分。

0%