# 闭包

函数即是数据。就比方说,函数可以赋值给变量,可以当参数传递给其他函数,还可以从函数里返回等等。这类函数有特殊的名字和结构

函数式参数(“Funarg”): 是指值为函数的参数

例子:

function exampleFunc(funArg) {
  funArg();
}

exampleFunc(function () {
  alert('funArg');
});

高阶函数(high-order function 简称:HOF): 指接受函数式参数的函数。 上述例子中 exampleFunc 就是这样的函数

带函数值的函数: 以函数作为返回值的函数

自由变量

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

function testFn() {

  var localVar = 10;

  function innerFn(innerParam) {
    alert(innerParam + localVar);
  }

  return innerFn;
}

var someFn = testFn();
someFn(20); // 30

上述例子中,对于 innerFn 函数来说,localVar 就属于自由变量

# 什么是闭包

闭包是代码块和创建该代码块的上下文中数据的结合

让我们来看下面这个例子(伪代码):

var x = 20;

function foo() {
  alert(x); // 自由变量"x" == 20
}

// 伪代码 foo闭包
fooClosure = {
  call: foo // 引用到function
  lexicalEnvironment: {x: 20} // 搜索上下文的上下文
};

上述例子中,fooClosure 部分是伪代码。对应的,在ECMAScript中,foo 函数已经有了一个内部属性——创建该函数上下文的作用域链。

lexical 通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量 “z” 的值总是 10

# ECMAScript闭包的实现

首先回顾一下 ECMAScript 的静态(词法)作用域

var z = 10;

function foo() {
  alert(z);
}

foo(); // 10 – 使用静态和动态作用域的时候

(function () {

  var z = 20;
  foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域

})();

// 将foo作为参数的时候是一样的
(function (funArg) {

  var z = 30;
  funArg(); // 10 – 静态作用域, 30 – 动态作用域

})(foo);

静态(词法)作用域意味着作用域在函数创建时创建的,且在之后的代码运行过程中不会被改变

回到正题,先来一个例子:

var x = 10;

function foo() {
  alert(x);
}

(function (funArg) {

  var x = 20;

  // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了
  funArg(); // 10, 而不是20

})(foo);

技术上说,创建该函数的父级上下文的数据是保存在函数的内部属性 [[Scope]] 中,回到上文说的什么是闭包:闭包是代码块和创建该代码块的上下文中数据的结合,上面的例子上代码是 foo ,创建 foo 的上下文是全全局上下文, 所以换句话说在 ECMAScript 中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链

var x = 10;

function foo() {
  alert(x);
}

// foo是闭包
foo: <FunctionObject> = {
  [[Call]]: <code block of foo>,
  [[Scope]]: [
    global: {
      x: 10
    }
  ],
  ... // 其它属性
};

同一个父上下文中创建的闭包是共用一个 [[Scope]] 属性的

也就是说,某个闭包对其中 [[Scope]] 的变量做修改会影响到其他闭包对其变量的读取:

var firstClosure;
var secondClosure;

function foo() {

  var x = 1;

  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };

  x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中

  alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}

foo();

alert(firstClosure()); // 4
alert(secondClosure()); // 3

这也是如果我们在循环语句里创建函数(内部进行计数)的时候经常得不到预期的结果的原因

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2

上述例子就证明了 —— 同一个上下文中创建的闭包是共用一个 [[Scope]] 属性的。因此上层上下文中的变量 “k” 是可以很容易就被改变的

activeContext.Scope = [
  ... // 其它变量对象
  {data: [...], k: 3} // 活动对象
];

data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

这样一来,在函数激活的时候,最终使用到的k就已经变成了 3 了。如下所示,创建一个闭包就可以解决这个问题了

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      alert(x);
    };
  })(k); // 传入"k"值
}

// 现在结果是正确的了
data[0](); // 0
data[1](); // 1
data[2](); // 2

函数 _helper 创建出来之后,通过传入参数 “k” 激活。其返回值也是个函数,该函数保存在对应的数组元素中。在函数激活时,每次 _helper 都会创建一个新的变量对象,其中含有参数 “x”“x” 的值就是传递进来的 “k” 的值。这样一来,返回的函数的 [[Scope]] 就成了如下所示:


data[0].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 0}
];

data[1].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 1}
];

data[2].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 2}
];

我们看到,这时函数的 [[Scope]] 属性就有了真正想要的值了,为了达到这样的目的,我们不得不在 [[Scope]] 中创建额外的变量对象。要注意的是,在返回的函数中,如果要获取 “k” 的值,那么该值还是会是 3

var data = [];

for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // 将k作为函数的一个属性
}

// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2

通过其他方式也可以获得正确的 “k” 的值,如下所示:

var data = [];

for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // 将k作为函数的一个属性
}

// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2

# 总结

ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  • 从实践角度:以下函数才算是闭包:

    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)

    • 在代码中引用了自由变量