深入理解闭包及原理

写在最前

老是觉得很难说清楚闭包,也觉得文章写的不够清楚,更新一下几个月前写的文章,便于加深理解。

有几个亘古不变的问题需要梳理下:

  1. 什么是闭包?
  2. 闭包的原理可不可以说一下?
  3. 你是怎样使用闭包的?

什么是闭包?

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

通过变相引用函数的活动对象导致其不能被回收,从而生成的依然可以用引用访问其作用域链的函数,被称为闭包。

换句话说:闭包是指有权访问另一个函数作用域中的变量的函数

闭包的原理可不可以说一下?

讲到原理,我们先需要搞懂一些基本的概念:

变量对象

如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道应该如何访问,这种机制称为变量对象(Variable Object)。

「变量对象」是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:

  • 函数的形参

    TIP

    名称对应值组成的一个变量对象的属性被创建;没有传递对应参数的话,那么由名称和 undefined 值组成的一种变量对象的属性也将被创建。

  • 函数内的变量声明

    TIP

    由名称和对应值(undefined)组成一个变量对象的属性被创建;如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

  • 函数内的函数声明(由 function 声明,包含其代码块,相当于整个函数)

    TIP

    由名称和对应值(函数声明 Function Declaration)组成一个变量对象的属性被创建;如果变量对象已经存在相同名称的属性,则完全替换这个属性。(换句话说,就是函数声明的优先级比变量声明优先级大

    ❗️需要注意的是,函数表达式并不能包含其中,如 (function test(){}),因为它不是一个函数声明。

它在函数执行前被创建,创建「变量对象」的过程其实就是函数内部数据(参数、变量、内部函数)的初始化过程。环境中定义的所有变量和函数都保存在这个对象里。

特殊的,在全局上下文环境中,全局对象(window)自身就是变量对象(VO)。

举例:

var a = 10
function test(x) {
  var b = 2 * x
}
text(20)

/* 对应的变量对象如下 */

// 全局上下文的变量对象
VO(globalContext) = {
  a: 10,
  test: Function
}
// test 函数上下文的变量对象
VO(test functionContext) = {
  x: 30,
  b: 20
};

活动对象

在函数执行上下文中,变量对象(VO)是不能直接访问的,此时由活动对象(Activation Object)代替扮演 VO 的角色。

相较于 VO,AO 是在进入函数上下文的时候时候开始创建的,可以提供给函数上下文中作为对象使用。

执行上下文

这是一个核心的概念,函数的每次执行,都会创建一个活动对象(AO)。

ES6 之前,JavaScript 只能通过「函数」(function)进行执行上下文的创建。

执行上下文的代码被分为两个阶段进行处理:

  1. 进入执行上下文

    TIP

    在进入执行上下文的时候(执行代码前),VO 已经包含了「形参」、「变量声明」、「函数声明」这些属性,这时候AO 也会创建一个相同内容的对象。

  2. 执行代码

    TIP

    在执行代码的这个周期内,VO 和 AO 已经拥有了对应的属性,需要注意的是,在这之前,除了拥有传参的形参,其他的变量声明对应的值都是 undefined,函数变量对应的值只是声明(Declaration)

    只有开始执行代码的时候,对应的参数、变量、函数变量才会被赋值。

举一个经典的例子:

console.log(x) // function
var x = 10
console.log(x) // 10
x = 20
function x() {}
console.log(x) // 20

上面代码在创建 VO 的时候,是这样的:

  1. 找到 var x,将 x 声明为变量;
  2. 找到 function x() {} 函数声明,x 被赋值为函数;
  3. 执行 x = 10
  4. 执行 x = 20

可以看到,同一周期,变量声明在顺序上是位于在函数声明和形式参数声明之后的,而且在这个进入上下文阶段,变量声明不会干扰 VO 中已经存在的同名函数声明或形式参数声明,因此,在进入上下文时,变量声明的优先级最低。

相当于

var x
function x() {}
console.log(x) // function
x = 10
console.log(x) // 10
x = 20
console.log(x) // 20

// or
function x() {}
var x
console.log(x) // function
x = 10
console.log(x) // 10
x = 20
console.log(x) // 20
// 因为函数声明优先级大,变量声明没有办法影响同名的函数声明或形参声明

另外,函数声明也可以覆盖形参声明:

function t(age) {
  console.log(age)
  var age = 99
  console.log(age)
  function age() {}
  console.log(age)
}
t(5)
// function age()
// 99
// 99

/* or */
function test(x) {
  x()
  function x() {
    console.log('bar')
  }
}
test('foo')
// bar

再来一个例子:

if (true) { var a = 1 }
else { var b = 2 }
 
console.log(a) // 1
console.log(b) // undefined 不是 b 没有声明,而是 b 的值是 undefined

可以看到,b 还是被声明了,因为 VO 是在进入执行上下文的时候被创建,不论所在的代码是否能够执行(如上 else 中的代码),变量 b 仍然存在于 VO 中。

作用域链

  • 执行环境

    执行环境定义了变量或函数有权访问的其他数据。

    在 web 浏览器的环境栈中,全局环境(window)永远都是最外层的「执行环境」,然后在每个函数被调用的时候,函数对应的执行环境会被 push 到环境栈中;只有当他以及依赖于它的成员都执行完毕后,该环境就会被 pop 出栈。

  • 作用域链

    在代码在一个环境中执行的时候,会创建一个变量对象的一个作用域链。

    作用域链的前端,始终都是当前执行的代码所在环境的「变量对象」。全局执行环境的「变量对象」也始终都是链的最后一个对象。

    作用域链其实就是引用了当前执行环境的「变量对象」的指针列表,它只是引用,但不是包含。

    举个例子:

    function foo(){
      var a = 12
      fun(a)
      function fun(a){
        var b = 8
        console.log(a + b)
      }
    }
    foo()
    

    这段代码的执行流程:

    1. 在创建 foo 的时候,作用域链已经预先包含了一个全局对象,并保存在内部属性 [[Scope]] 当中。
    2. 执行 foo 函数,创建执行环境与活动对象后,取出函数的内部属性 [[Scope]] 构建当前环境的作用域链(取出后,只有全局变量对象,然后此时追加了一个它自己的活动对象)。
    3. 执行过程中遇到了 fun,从而继续对 fun 使用上一步的操作。
    4. fun 执行结束,移出环境栈。foo 因此也执行完毕,继续移出。
    5. javscript 监听到 foo 没有被任何变量所引用,开始实施垃圾回收机制,清空占用内存。

面试题

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m) {
      return fun(m, n)
    }
  }
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3); // undefined,0,1,2
var c = fun(0).fun(1); c.fun(2); c.fun(3); // undefined,0,1,1

总结

  1. 什么是闭包?

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

    通过变相引用函数的活动对象导致其不能被回收,然而形成了依然可以用引用访问其作用域链的结果。

  2. 闭包的原理可不可以说一下?

    结合我们上面讲过的,它的根源起始于词法阶段,在这个阶段中形成了词法作用域。最终根据调用环境产生的环境栈来形成了一个由变量对象组成的作用域链,当一个环境没有被 JS 正常垃圾回收时,我们依然可以通过引用来访问它原始的作用域链。

  3. 你是怎样使用闭包的?

    这就是智者见智了~ 比如:柯里化

参考资料

深入贯彻闭包思想,全面理解JS闭包形成过程 - Base JavaScript - SegmentFault 思否

JavaScript内部原理系列-闭包(Closures) - kidsamong - SegmentFault 思否

搞懂闭包 | AlloyTeam

深入理解JavaScript系列(12):变量对象(Variable Object) - 汤姆大叔 - 博客园

上次更新: 8/18/2019, 4:04:46 PM