js-执行上下文和执行栈

192 阅读6分钟

参考文章

什么是执行上下文

简而言之,执行上下文是评估和执行js代码的环境的抽象概念,每当js代码运行的时候,它都是在执行上下文中运行的。

执行上下文的类型

js中的执行上下文分为三种类型:

  • 全局执行上下文
  • 函数执行上下文
  • Eval函数执行上下文

全局执行上下文

该类型是默认类型,或者说是基础的上下文,任何不在函数内部的代码都在全局上下文中。

它会执行两件事:

    • 创建一个全局的window对象(浏览器的情况下)
    • 设置this的值等于这个全局对象。

注意:一个程序中只会有一个全局执行上下文

函数执行上下文

每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以任意多个的。每当一个新的执行上下文被创建,它会按定义的顺序(后进先出,将在后文讨论)执行一系列步骤。

Eval 函数执行上下文

执行在eval函数内部的代码也会有它属于自己的执行上下文,但由于js开发者并不经常使用eval,所以不具体研究讨论

执行栈

执行栈是用来管理执行上下文的,存储js代码执行时创建的所有执行上下文,是一种遵循LIFO(先进后出)数据结构的栈,上文函数执行上下文中的执行顺序便是按照执行栈的顺序。

注:几乎所有的栈都是遵循先进后出原则

当js引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并压入当前执行栈。每当引擎遇到一个函数调用时,它都会为该函数创建一个新的执行上下文并压入栈的顶部。

执行上下文的生命周期

执行上下文的生命周期包括三个阶段:创建阶段➡️执行阶段➡️回收阶段

function a(){
    console.log(1);
}

function b(){
    a();
    console.log(2);
}
b();
console.log(3);

接下来通过上述代码,来具体阐述下执行上下文、执行栈及其什么周期

  1. 当上述代码在浏览器加载时,js引擎创建一个全局执行上下文,并把它压入当前执行栈(压栈)
  2. 当遇到b()函数调用时,js引擎会为创建一个新的函数执行上下文,并把它压入当前执行栈的顶部。
  1. 当从b()函数内部调用a()函数时,js引擎为a()函数创建一个全新的函数执行上下文并把它压入当前执行栈的顶部
  2. a()函数执行完毕,将它的执行上下文会从当前执行栈弹出(出栈),并控制流程进入下一个执行上下文,即b()函数的执行上下文
  1. b()函数执行完毕,并将它的执行上下文从当前执行栈弹出,控制流程达到全局执行上下文
  2. 一旦所有代码执行完毕,js引擎从当前栈移除全局执行上下文

创建阶段

接下来我们来具体看下创建阶段,在js代码执行前,执行上下文将经历创建阶段。

在创建阶段会发生三件事:

  1. this值的决定,即我们所熟悉的 this绑定 或者 this指向
  2. 创建词法环境,即作用域
  3. 创建变量环境

所以执行上下文在概念上表示如下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

this绑定(this指向)

  • 在全局执行上下文中,this的值指向全局对象。(在浏览器中,this引用Window对象)
  • 在函数执行上下中,this的值取决于函数是如何调用的。如果它被引用对象调用,那么this会被设置成那个对象,否则this的值会被设置为undefind(在严格模式下)

注:关于this绑定(this指向)的具体细节更多是和变量提升一起讨论的

创建词法环境

官方的 ES6 文档把词法环境定义如下

词法环境是一种规范类型,基于ECMAScript代码的词法嵌套结构来定义标识符、具体变量和函数关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

简单来说词法环境是一种持有标识符-变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象【包含函数类型对象】或原始数据的引用)。

现在,在词法环境的内部有两个组件

  1. 环境记录器:是存储变量和函数声明的实际位置
  2. 外部环境的引用:意味着它可以访问其父级词法环境(作用域)

词法环境有两种类型

  1. 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是null。它拥有内建的object/array/等、在环境记录器内的原始函数(关联全局对象,比如 window 对象)还有任何用户定义的全局环境,并且this的值指向全局对象
  2. 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型:

  1. 声明式环境记录器存储变量、函数和参数
  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系

简而言之,

  • 全局环境中,环境记录器是对象环境记录器。
  • 函数环境中,环境记录器是声明式环境记录器。

注:对于函数环境,声明式环境记录器还包含了一个传递给函数的arguments对象(此对象存储索引和参数的映射)和传递给函数的参数的lenth。

创建变量环境

它同样也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

简而言之,变量环境也是一个词法环境,它有着上面定义的词法环境的所有属性。

在es6中,词法环境和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 count )绑定,而后者只用来存储var变量绑定。

执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码。

注:在执行阶段,如果js引擎不能在源码中声明的实际位置找到let变量的值,它会被赋值为undefuned

回收阶段

执行上下文出栈等待虚拟机回收执行上下文

总结

上文已经对执行上下文和执行栈进行来详细的阐述,虽然要成为一名卓越的 JavaScript 开发者并不需要学会全部这些概念,但是如果对上面概念能有不错的理解将有助于你更轻松,更深入地理解其他概念,如变量声明提升,作用域和闭包。