JS核心概念——执行上下文

368 阅读6分钟

1. 执行上下文是什么

简单来说,执行上下文(Execution Context)是指一段 JS 代码在执行时的环境信息(变量、函数、参数、作用域链、this等信息)。

2.执行上下文的分类

  • 全局执行上下文:当 JS 程序开始运行时,会自动创建一个全局执行上下文。它代表了整个程序的运行环境,并在整个程序运行期间一直存在。默认会在全局内存中创建两个属性: 1.创建一个 window 对象指向 global 对象 2.将 this 指向 window 对象。

  • 函数执行上下文:每当调用一个函数时,都会创建一个新的函数执行上下文。函数执行上下文只在函数执行期间存在,并在函数执行完成后被销毁。默认会创建 arguments 和 this 对象 ,arguments 默认为 { length: 0 },this 的值因调用函数的方式而异。

  • 模块执行上下文:当 JS 文件作为模块被导入时,就会创建一个新的模块执行上下文。每个模块都有自己的执行上下文,这意味着每个模块的变量和函数都是私有的,并且不会与其他模块的变量和函数发生冲突。模块执行上下文将在程序退出、模块被替换、 模块不再被引用时被销毁。

  • eval执行上下文:使用 eval 函数执行 JS 代码,则会创建一个 eval 函数执行上下文。

3.执行上下文的结构

  • 词法环境(LexicalEnvironment):用来保存标识符与变量值(或函数值)的关系,这个过程叫做绑定。标识符是指变量或函数的名称,变量值是对实际对象或原始值的引用。词法环境由环境记录(EnvironmentRecord)和对外部词法环境(OuterEnv)的引用组成。

  • 变量环境(VariableEnvironment):它与词法环境类似,它定义了标识符与变量值(而非函数值)的关系。

主要区别: Lexical Environment 用于存储标识符与变量 (由let和const声明的) 和函数值的绑定,而 Variable Environment 仅用于存储标识符与变量 (由var声明的) 值的绑定。

4.执行栈的结构

执行栈是一种后进先出的数据结构,用于保存代码执行时产生的多个执行上下文。JS 引擎通过它,将每个调用存储到内存中,来跟踪执行上下文。全局执行上下文默认存在于执行栈中,并且位于执行栈底部。在执行全局执行上下文代码时,如果 JS 引擎发现函数调用,它会为该函数创建一个函数执行上下文并将其推送到执行栈的顶部。一旦函数的所有代码都执行完毕,JS 引擎就会取出该函数的执行上下文,并继续执行下面的代码。

让我们通过一个例子来理解这一点: call-stack.gif

console.log("Initially I am inside the global execution context.");

let message = "Heyyow!";

function first() {
  console.log("I am inside the first function execution context");
  second();
  console.log("I am again inside the first function execution context");
}

function second() {
  console.log("I am inside the second function execution context");
}

first();

console.log("I am back at the global execution context.");

5.执行上下文的生命周期

每个执行上下文都有两个阶段:1.创建阶段 2.执行阶段

以下面这段代码为例:

var name = "Luigi";
let input = "Hello, World!";

function broadcast(message) {
  return `${name} says ${message}`;
}

console.log(broadcast(input));

1.创建阶段

创建阶段的执行上下文的结构如下(伪代码):

GlobalExecutionContext = {
  LexicalEnvironment : { // 绑定由 let、const 声明的变量,以及函数声明
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord : {
        input: <uninitialized>,
        broadcast: < function broadcast(message) {
          return `${name} says ${message}`;
        } >,
      }, 
      ObjectEnvironmentRecord : {
        window: < ref. to Global obj. >,
        this: < ref. to window obj. >,
      },
      OuterEnv : < null >, // 引用父级 LexicalEnvironment,全局上下文的父级 LexicalEnvironment  null
    }, 
  },
  VariableEnvironment : { // 绑定由 var 声明的变量
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord : {
        name: undefined,
      }, 
    },
  },
}
  1. JS 引擎进入创建阶段
  2. 创建全局执行上下文并将其推入执行上下文栈的栈顶
  3. 为 window 对象创建到 Global 对象的绑定
  4. 为 this 对象创建到 window 对象的绑定,this的值根据函数具体调用方式而有所不同
  5. 在全局内存中创建一个标识符 name 并用值 undefined 初始化它,这个过程称为提升(hoisting)
  6. 在全局内存中创建一个标识符 input,而不对其进行初始化(不设置初始值)
  7. 在全局内存中创建一个标识符 broadcast,并将 broadcast 函数的整个函数定义存储在其中,这个函数也被提升

2.执行阶段

这是 JS 引擎在声明了所有的变量和函数,必要的对象已经绑定之后进入的阶段。每个执行上下文都有一个执行阶段。这个阶段发生的事情很少:变量绑定初始化、变量赋值、可变性和不可变性检查、变量绑定删除、函数调用执行等。

执行阶段的执行上下文的结构如下(伪代码):

GlobalExecutionContext = {
  LexicalEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord: {
        input: "Hello, World!",
        broadcast: {
          LocalExecutionContext : {
            LexicalEnvironment : {
              EnvironmentRecord: {
                DeclarativeEnvironmentRecord: {
                  message: "Hello, World!",
                },
                ObjectEnvironmentRecord: {
                  arguments: { 0: message, length: 1 }
                  this: < ref. to window obj. >
                },
              },
              OuterEnv: < ref. to LexicalEnvironment of the GlobalExecutionContext >,
            },
          },
        },
      },
      ObjectEnvironmentRecord: {
        window: < ref. to Global obj. >,
        this: < ref. to window Obj. >,
      },
      OuterEnv: < null >,
    },
  },
  VariableEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord: {
        name: "Luigi"
      },
    },
  },
}
  1. JS引擎进入执行阶段
  2. 获取变量 name 的值并将该值绑定到内存中的标识符
  3. 获取变量 input 的值并将该值绑定到内存中的标识符
  4. 遇到 console.log 方法,立即评估其中的参数
  5. 遇到 broadcast 函数调用,立即为该函数创建一个新的函数执行上下文,并推入执行上下文栈栈顶
  6. 进入 broadcast 函数执行上下文的创建阶段
  7. 在该函数的本地内存中创建参数(arguments)对象,初始值为: { length: 0 }
  8. 将传入的参数 message 添加到参数对象的第一个索引: { 0: message, length: 1 }
  9. 在函数的本地内存中创建标识符 message,并保存传递给函数调用参数的值
  10. 进入函数体内部,评估返回语句的返回值
  11. 看到变量 name,然后在函数的本地内存中查找该变量
  12. 无法在本地内存中找到标识符 name,因此会继续从其父范围(全局内存)中查找 name
  13. 在全局内存中找到标识符 name,因此采用该值来替换 name 变量
  14. 看到变量 message,然后在函数的本地内存中查找该变量
  15. 它在本地内存中找到标识符 message,因此用该值来替换 message 变量
  16. 返回 broadcast 函数执行上下文的结果,并将其从执行上下文栈中弹出
  17. 将控制权与返回结果一起传递给它的调用上下文(全局执行上下文)
  18. 显示 Luigi says Hello, World!在控制台中
  19. 全局执行上下文从调用堆栈中弹出,然后 JS 引擎退出

为了帮你进一步理解执行上下文的整个生命周期,请看下图:

execution-context.gif

相信对上述概念的理解,将有助于你更深入地理解这些概念:提升、作用域链、闭包、this。

参考资料:
blog.openreplay.com/explaining-…
blog.bitsrc.io/understandi…
dev.to/luigircruz/…