闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。

用处在于它可以将一些数据和操作它的函数关联起来。这和面向对象编程明显相似。在面对象编程中,我们可以将某些数据(对象的属性)与一个或者多个方法相关联。

下面的每段代码都经过测试。


一、封装私有变量

var makeCounter = function() {
  var privateCounter = 0

  return {
    increment: function() {
      privateCounter++
    },
    value: function() {
      return privateCounter
    }
  }  
}

var Counter1 = makeCounter()
var Counter2 = makeCounter()

Counter1.increment()
Counter1.increment()
Counter1.value() // 2
Counter2.value() // 0

每个闭包都是引用自己词法作用域内的变量 privateCounter。以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

另外这里还可用 WeakSet 优化。

二、匿名自执行函数

var data= {    
  table : [],    
  tree : {}    
}

(function(dm) {
  for(var i = 0; i < dm.table.rows; i++) {    
    var row = dm.table.rows[i]
    for(var j = 0; j < row.cells; i++) {
      drawCell(i, j)
    }    
  }
})(data)

函数执行完后会立刻释放资源,不污染全局对象。

三、结果缓存

var CachedSearchBox = (function () {
  var cache = {}

  return {
    attachSearchBox: function (dsid) {
      if (dsid in cache) 
        return cache[dsid]

      cache[dsid] = ...

      return cache[dsid]
    }
  }
})()

四、模块化

如实现一个同步加载的 AMD 模块:

var MyModules = (function Manager() {
  var modules = {};

  function define(name, deps, impl) {
    for (var i=0; i<deps.length; i++)
      deps[i] = modules[deps[i]]
		
    modules[name] = impl.apply( impl, deps )
  }

  function get(name) {
    return modules[name]
  }

  return {
    define: define,
    get: get
  }
})()

MyModules.define( "bar", [], function(){
  function hello(who) {
    return "Let me introduce: " + who
  }

  return {
    hello: hello
  }
})

MyModules.define( "foo", ["bar"], function(bar){
  var hungry = "hippo"

  function awesome() {
    console.log( bar.hello( hungry ).toUpperCase() )
  }

  return {
    awesome: awesome
  }
})

var bar = MyModules.get( "bar" )
var foo = MyModules.get( "foo" )

console.log(bar.hello( "hippo" )) // Let me introduce: hippo
foo.awesome() // LET ME INTRODUCE: HIPPO

而 ES6 模块就是在其模块文件内部的内容被视为像是包围在一个作用域闭包中。

NodeJS 中的模块实现:

function require(...) {
  var module = { exports: {} };
  ((module, exports) => {
    // Your module code here. In this example, 
    // define a function.
    function some_func() {};
    exports = some_func;
    // At this point, exports is no longer a shortcut 
    // to module.exports, and this module will still 
    // export an empty default object.
    module.exports = some_func;
    // At this point, the module will now export 
    // some_func, instead of the default object.
  })(module, module.exports);
  return module.exports;
}

五、回调

因为词法作用域被保留,回调可同步可异步,这在监听 DOM 操作中十分有用。

六、导出内部变量

将内部变量导出,给其他函数使用。

七、柯里化

把差异化参数提前固化,这样不用每次调用的时候都传递参数了。

const plus = a => b => a + b
const plus1 = plus(1)
plus1(1) // 2

我们需要实现自动柯里化的函数:

const curry = fn => {
  const _c = (restNum, argsList) => restNum === 0 ?
    fn(...argsList) : x => _c(restNum - 1, [...argsList, x])

  return _c(fn.length, [])
}

const plus = curry((a, b, c) => a + b + c)
plus(1)(1)(1) // 3

常见问题

在循环中创建闭包

var a=[]
for(var i=0; i<10; i++)
  a[i] = () => console.log(i)

a[6]() // 10  

变量 i 在全局作用域内,匿名函数自己又没有 i,于是绑定了 i 的引用,而 i 在循环中不断被覆盖,所以 i 为 10 而不是 6。

如果用自执行函数 (IIFE) 的写法:

var a=[]
for(var i=0; i<10; i++) {
  (function() {
    var j=i
    a[i] = () => console.log(j)
  })()
}

a[6]() // 6

或者:

var a=[]
for(var i=0; i<10; i++) {
  (function(j) {
    a[i] = () => console.log(j)
  })(i)
}

a[6]() // 6

IIFE 需要它自己的变量,在每次迭代时持有值 i 的一个拷贝,里面的函数发现自己没有 j,于是从 IIFE 里面去要。

在每次迭代内部使用的 IIFE 为每次迭代创建了新的作用域,这些作用域中的每一个都拥有一个持有正确的迭代值的变量给我们访问。因为作用域被引用了,这个作用域不会被释放掉。

let 方式:

var a=[]
for(var i=0; i<10; i++) {
  let j=i
  a[i] = () => console.log(j)
}

a[6]() // 6

变量 i 是 let 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量,这时每个闭包才会捕获不同的 i。

根据 ECMA 6 specification,对这种特定形式的声明:

for(let i;;) { }

JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。

var a=[]
for(let i=0; i<10; i++)
  a[i] = () => console.log(i)

a[6]() // 6

注意性能

在不是必需的情况下,在其它函数中创建函数是不明智的。因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

比如,在创建新的对象或者类时,方法通常应该关联到对象的原型,而不是定义到对象的构造器中。因为这将导致每次构造器被调用,方法都会被重新赋值一次。

内存问题

function out() {
  const bigData = new Buffer(100)
  inner = function () {
    void bigData
  }
}

闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。上面例子是 inner 直接挂在了 root 上,从而导致内存泄漏(bigData 不会释放)。

需要注意的是,这里举得例子只是简单的将引用挂在全局对象上,实际的业务情况可能是挂在某个可以从 root 追溯到的对象上导致的。

参考