理解 JS 中闭包的概念

有了之前作用域链的知识。我们对 [[Scope]] 和作用域链的知识完全理解了的话,那对闭包也就很好理解了。

我们通过实例来分析:一个闭包内部的函数,它是如何去访问到其外部函数作用域中的变量(即使外部函数已经执行完毕)的,也就能明白闭包的原理了。

var scope = "global scope";

function checkscope() {
  var scope = "local scope";

  function f() {
    return scope;
  }
  return f;
}

var foo = checkscope();
foo(); // local scope

先仔细分析上面这段代码的执行过程:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈;
  2. 全局执行上下文初始化;
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈;
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等;
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出;
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈;
  7. f 执行上下文初始化,创建变量对象、作用域链、this等;
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出;

再来看看 f 函数在执行时,是如何访问到它外部函数checkscope作用域内的变量scope

在 checkscope 函数内部定义的函数 f 会将 checkscope 函数的活动对象添加到它的作用域链中。具体来说的话:

  • f 函数在创建时,会把它所有父变量对象保存到其内部属性[[scope]]
f.[[scope]] = [checkscopeContextAO ,gobalContextVO]
  • 然后,在 f 函数调用时,将引用 [[scope]] 中的对象,构建起执行上下文中的作用域链。再把 f 函数的活动对象推入这个作用域链前端。

这样 f 函数就可以访问在 checkscope 中定义的变量 scope 了。

但更为重要的是,在 checkscope 执行结束之后,其活动对象并不会被销毁,因为 f 函数的作用域链仍然在引用这个活动对象。换句话说,当 checkscope 函数在返回(return) f 函数后,其执行环境的作用域链会被销毁,但它的活动对象仍然保留在内存中。直到 f 函数被销毁,checkscope 的活动对象才会被销毁。 正是因为这个原因,f 函数在 checkscope 执行完之后依然可以访问在 checkscope 中定义的变量 scope

闭包的定义

此时的 f 函数就是一个闭包,让我们再回头看看闭包的实践角度定义:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量(自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量,在上面的例子 checkscope 中声明的scope变量,在 f 函数中就叫自由变量)

参考资料