闭包的文章看了许多,直到看到了《你不知道的JS》中对闭包的阐述,才让我真正理解闭包。
由下面一个例子引出闭包的概念,

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

当我们执行baz()的时候,很神奇的,居然可以访问的function foo()内部的局部变量a。显然,变量a在自身的词法作用域外被访问。
foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为引擎由gc来释放不再使用的内存空间,执行过的foo()显然应该是被gc回收的对象。
然而,闭包神奇之处正式可以阻止这件事的发生,事实上,foo()的作用域依然存在,而访问这个作用域的正是bar函数本身。
bar函数有对该作用域的引用,这个引用就是闭包。(引用是闭包,闭包不一定是引用)
总结一下:
当函数可以记住并访问所在的词法作用域(静态作用域),即使函数是在当前词法作用域之外执行,这时就产生了闭包。

下面举出多个例子来帮加深理解闭包。

case1

  1. function foo() {
  2. var a = 2;
  3. function bar() {
  4. console.log(a) // 闭包
  5. }
  6. bar();
  7. }
  8. foo();
  9. /*************************************/
  10. var name = '葡萄';
  11. function eat() {
  12. console.log( name ); // 闭包
  13. }
  14. eat(); // '葡萄'

case2

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

注意,词法作用域是编译时根据书写位置决定的,不是在执行的时候决定的。在此例中,baz通过参数传递给bar函数中作为fn函数执行,此时baz执行是在其词法作用域之外,且能够访问baz定义时的词法作用域。因此产生了闭包。

case3

  1. var fn;
  2. function foo() {
  3. var a = 2;
  4. function baz() {
  5. // console.log(a) // 2
  6. }
  7. fn = baz
  8. }
  9. function bar() {
  10. fn() // 闭包
  11. }
  12. foo();
  13. bar();

同理,baz执行时在其词法作用域之外,且能够访问baz定义时的词法作用域。

case4

  1. function wait(message) {
  2. setTimeout(function () {
  3. console.log(message); // 闭包
  4. }, 1000)
  5. }
  6. wait("Hello, closure!")

首先,在seTimeout函数内部的回调函数访问了词法作用域上的message。其次,调用时机由于回调函数的控制反转是在其他地方调用的,因此是在该回调函数词法作用域之外执行。
由此可见,回调函数基本都是在使用闭包的特性。

case5

  1. var a = 2;
  2. (function (){
  3. console.log(a) // 闭包
  4. }())

虽然不满足在词法作用域外调用,但是它满足了访问当前词法作用域的条件,因此产生了闭包。

case6

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

分析:
关于定时器如何产生闭包之前已经分析过了。当回调函数执行的时候,此时的词法作用域中i的值已经是6.因此输出的全是6.

如何改进?
改进1:
为每一次迭代都单独创建一个闭包,通过立即执行函数创建一个函数作用域,在作用域内保存当前i的值。之后每轮迭代对应的回调函数执行时,在词法作用域内能够查找到当时保存的i值。

  1. for (var i = 1; i <= 5; i++) {
  2. (function (j) {
  3. setTimeout(function () {
  4. console.log(j)
  5. }, i * 1000);
  6. })(i)
  7. }
  8. // 结果:
  9. // 1 2 3 4 5

改进2:
利用let的特性,在循环的每一轮迭代中会保存当前的副本。

  1. for (let i = 1; i <= 5; i++) {
  2. setTimeout(function () {
  3. console.log(i)
  4. }, i * 1000);
  5. }
  6. // 结果:
  7. // 1 2 3 4 5