闭包closure

闭包是代码块和创建该代码块的上下文中数据的结合. 这句话比较拗口, 通俗地说, 定义在闭包中的函数可以记忆它创建时刻的环境. 通过闭包, 函数外部的变量可以访问到内部的变量.

常见形式

在一个函数内创建另一个函数, 通过另一个函数访问这个函数的局部变量.

function foo() {
  var a = 1;
  return function(){
    console.log(a++);
  }
}

var bar = foo();
bar();  // 输出1
bar();  // 输出2
bar = null; // a被回收

原理

主要还是基于js的函数作用域链以及垃圾回收的原理, 在函数执行结束后, 与之相关的作用域链并没有消失.

C语言中, 函数的局部变量定义在CPU的栈中, 当函数返回时, 局部变量确实不存在了. 但JS中, 作用域链是一个对象列表而非栈.

每次调用函数的时候, 都会为这个函数创建一个活动对象来保存局部变量, 并把这个对象添加到作用域链中. 当函数返回的时候, 就从作用域链中将这个绑定变量的对象删除. 如果不存在嵌套函数, 也不存在其它引用指向这个绑定对象, 它就会被当作垃圾回收. 如果定义了嵌套函数, 每个嵌套函数都会各自对应一个作用域链, 并且这个作用域链指向一个变量绑定对象. 如果这些嵌套的函数对象在外部函数中保存了下来, 那么他们也会和所指向的变量绑定对象一样, 被当做垃圾回收.

但如果外层函数将嵌套函数作为返回值返回或存储在某处属性中, 这时就会有一个外部引用指向这个嵌套的函数, 嵌套函数就不会被当做垃圾回收, 并且它所指向的变量绑定对象也不会被回收. 因为作用域链是函数定义的时候创建的, 不管何时何地执行f(), 这种绑定依然有效.

即, 闭包可以捕获到局部变量和参数, 并一直保存下来, 看起来就像这些变量绑定到了在其中定义它们的外部函数.

注意, 关联到闭包的作用域链都是"活动的", 嵌套的函数不会将作用域内的私有成员复制一份, 也不会对绑定的变量生成静态快照.

特点

可以看出, 闭包的特点就是

  • 函数内部可以引用外部的参数和变量, 且参数和变量不会被GC回收.
  • 常驻内存, 会增大内存消耗, 使用不当容易内存泄露

用途

  • 希望一个变量常驻内存
  • 避免全局变量污染
  • 设计私有的变量和方法

用闭包模拟私有方法/模块模式

var Counter = (function() {
  var privateCounter = 0;  // 模拟了私有属性, 只能通过暴露的接口操作
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

alert(Counter.value()); /* 提示 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* 提示 2 */
Counter.decrement();
alert(Counter.value()); /* 提示 1 */

闭包的一个坑

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 5);
}

上面这个代码块会打印五个 5 出来, 而我们预想的结果是打印 1 2 3 4 5.

之所以会这样, 是因为 setTimeout 中的 i 是对外层 i 的引用. 当 setTimeout 的代码被解释的时候, 运行时只是记录了 i 的引用, 而不是值. 而当 setTimeout 被触发时, 五个 setTimeout 中的 i 同时被取值, 由于它们都指向了外层的同一个 i, 而那个 i 的值在迭代完成时为 5, 所以打印了五次 5.

为了得到我们预想的结果, 我们可以把 i 赋值成一个局部的变量, 从而摆脱外层迭代的影响.

for (var i = 0; i < 5; i++) {
  (function (idx) {
    setTimeout(function () {
      console.log(idx);
    }, 5);
  })(i);
}

GC原理

顺便一提, js的垃圾回收机制(garbage collection)规定:

  • 一个对象不再被引用, GC回收之
  • 两个对象互相引用, 且不再被第三者引用

References