闭包的底层实现原理

JS 的执行上下文

JS的代码在之前之前,会被浏览的JS引擎进行一次预编译以后,才能被真正执行。而在预编译的过程中,引擎做的事情有:变量提升,函数提升。

  1. var d = 'abd';
  2. function a(){
  3. console.log('函数a')
  4. }
  5. console.log(a)
  6. a();
  7. var c = 123;
  8. // 在函数进行预编译以后相当于
  9. function a(){
  10. console.log('函数a')
  11. }
  12. var d;
  13. console.log(a) // function(){}
  14. a()
  15. a = '变量a' // 变量a赋值,函数声明被覆盖
  16. console.log(a)
  17. a(); // a is not a function

预编译的这个过程是在一个运行环境中执行的,而这个运行环境就是JS的执行期上下文。

执行上下文分为三种:

  • 全局执行期上下文:代码开始执行时首先进入的环境
  • 函数执行期上下文:函数调用时,会开始执行函数中的代码
  • eval执行期上下文:解析js字符串时确定的上下文

执行上下文的周期分为两个阶段:

  • 创建阶段

    • 创建词法环境
    • 生成变量对象,建立作用域链
    • 确定this指向,并同时绑定this
  • 执行阶段

    • 在这个阶段进行变量赋值。函数引用及执行代码

如图:

闭包的执行原理 - 图1

预编译发生在函数执行之前,预编译四部曲为:

  • 创建 AO 对象
  • 找形参和变量声明,将变量和形参作为 AO 属性名,值为 undefined
  • 将形参和实参统一(形参赋值)
  • 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从 AO 对象中拿。
  1. 什么叫变量对象?
    变量对象是 js 代码在进入执行期上下文时,JS 引擎在内存中简历的一个对象,用来存放当前执行环境中的变量。

  2. 变量对对象的创建过程
    变量对象的创建,是在执行期上下文创建阶段,依次经过一下三个过程:

    • 创建 arguments 对象。
      对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入 arguments 对象中,否则将参数名和 undefined 组成的键值对放入 arguments 对象中。
      //举个栗子 
      function bar(a, b, c) {
      console.log(arguments);  // [1, 2]
      console.log(arguments[2]); // undefined
      }
      bar(1,2)
      
  - 当遇到同名的函数时,后面的会覆盖前面的。
console.log(a); // function a() {console.log('Is a ?') }
function a() {
    console.log('Is a');
}
function a() {
  console.log('Is a ?')
}
  • 检查当前环境中的变量声明并复制为 undefined 。当遇到同名的函数声明,则会忽略,变量和函数都一样。重复的声明不会导致报错,在执行阶段会被忽略。
    console.log(a); // function a() {console.log('Is a ?') }
    console.log(b); // undefined
    function a() {
    console.log('Is a ');
    }
    function a() {
    console.log('Is a ?');
    }
    var b = 'Is b';
    
  1. 执行期上下文的第二个阶段,成为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时变量对象变为活动对象。
    console.log(a); // function a() {console.log('fjdsfs') }
    console.log(b); // undefined
    function a() {
    console.log('Is a');
    }
    function a() {
    console.log('Is a?');
    }
    var b = 'Is b';
    console.log(b); // 'Is b'
    var a = 10086; 
    console.log(a);  // 10086
    var b = 'Is b?';
    console.log(b); // 'Is b?'
    


在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:

// 创建过程
EC= {
  VO: {}; // 创建变量对象
  scopeChain: {}; // 作用域链
}
VO = {
  argument: {...}; // 当前为全局上下文,所以这个属性值是空的
  a: <a reference> // 函数 a  的引用地址
  b: undefiend  // 见上文创建变量对象的第三步
}

词法作用域(Lexical scope

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。`JavaScript`的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 `undefined`)。
 1: let top = 0; // 
 2: function createWarp() {
 3:   function add(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return add
 8: }
 9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result: ', result)

分析过程如下:

  • 在全局上下文中声明变量top 并赋值为0.

  • 2 - 8行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第3-7行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。

  • 第9行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined

  • 第9行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤2中创建完毕。接着,调用它。

  • 调用函数时,回到第2行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.

  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。

  • 第7行,我们返回变量 add 的内容。js引擎查找一个名为 add 的变量并找到它. 第4行和第5行括号之间的内容构成该函数定义。

  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相

  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。