执行上下文、Closure(面试版)

826 阅读3分钟

面试中大概被问了两次你知道执行上下文吗?我都回答不是很好。

  1. 是自己对概念确实理解不够深。
  2. 大概是从读书开始,就不喜欢背东西,更喜欢逻辑性强的东西

所以本文将口语(面试中回答)的解释什么是执行上下文和Closure如何创建的。所以本文可能标题啥的会比较弱化。

本文加粗字体为关键内容。。。。

首先我们知道JS中可以执行的代码主要分为三种全局代码函数eval

代码执行可以分为三个阶段 预编译(创建环境)代码执行垃圾回收

【如果面试的时候,怕说了过多的内容被面试官,可以直接解释一个函数的执行,无需说整体的】

预编译

在V8中每一个函数在执行前都会执行预编译,预编译主要有三步

1. CreateFunctionContext创建函数的执行上下文

每个执行上下文都有(ES5+):

  • this Binding
  • LexicalEnvironment(词法环境),存储函数声明和变量(let和const)绑定
  • VariableEnvironment(变量环境),存储变量(var)绑定。变量环境也是一个词法环境

ES1~ES5的执行上下文:

  • 变量对象VO。函数的所有形参、函数声明、变量声明
  • 作用域链(Scope chain)
  • this

2. PushContext上下文栈

多个执行上下文时,我们需要进行管理的,所以需要把执行上下文放入上下文执行栈中。

3. CreateClosure创建函数的闭包对象

每一个函数执行前都会创建一个闭包(这儿是和MDN概念对齐的),无论这个闭包是否使用,那是如何确定其内容的呢?
Closure[[Scopes]]一样会在函数预编译是被确定,区别在于

  • 当前函数的[[Scopes]]是在其父函数的预编译时确定,
  • Closure是在当前函数预编译时确定(在当前函数执行下文创建完成入栈后就开始创建闭包对象)

当V8预编译一个函数(outerFun)时,如果遇到内部函数(innerFun)的定义不会选择跳过,而是快速扫描内部函数innerFun中使用到的本函数(outerFun)LE中的变量,然后将这些变量的引用加入到Clousure对象。再来为这个内部函数绑定[[Scope]],并且使用当前函数的Clousre作为内部函数[[Scopes]]的一部分。

现在的V8中在为一个函数绑定词法作用域的时,并不会粗暴的的直接把函数的LE放入其[[Scope]]中,而是会分析这个函数中使用函数的LE中的那些变量,而这些可能会被使用到的变量会被存储在一个叫做Closure的对象中,每个函数都有且有一个Closure对象,最终这个Closure将会代替父函数的LE出现在函数[[Scopes]]

// innerFun在outerFun内部 定义并调用
innerFun执行时的作用域链是这样的:[innerFun.LE, outerFun.Closure, global.LE]

上下文展示

// 全局执⾏上下⽂
GlobalExectionContext = {
  thisBind: <this value>,
  // 词法环境
  LexicalEnvironment: {
    // 环境记录
    EnvironmentRecord: {
        Type: "Object", // 全局环境
        // ... 标识符绑定在这⾥
        outer: <null>, // 对外部环境的引⽤
      }
   }
}
// 函数执⾏上下⽂
FunctionExectionContext = {
  thisBind: <this value>,
  LexicalEnvironment: {
    EnvironmentRecord: {
    	Type: "Declarative",// 函数环境
    	// ... 标识符绑定在这⾥
    	// 对全局环境或外部函数环境的引⽤
    	outer: <Global or outer function environment reference>,
     }
   }
}

参考资料

一文颠覆大众对闭包的认知
搞懂变量提升、this、作用域链、闭包以及(GO,VO,AO)原理