深入理解JS之执行上下文(创建-执行-回收)

1,361 阅读6分钟

承接上文,本文讲解执行上下文的创建、执行和回收阶段,重点是创建和执行阶段,回收涉及到浏览器的内存回收策略,本文暂不涉及以后会提到。(本文主要总结自网络和书籍中,其中加上本人自己的总结和思考)

创建阶段

首先创建应该是在预解析阶段,这个阶段主要工作有:

  • 确定 this 的值,也被称为 This Binding。
  • LexicalEnvironment(词法环境) 组件被创建。
  • VariableEnvironment(变量环境) 组件被创建。

直接看伪代码可能更加直观:

ExecutionContext = {
    ThisBinding = '< this value >’, // this确定
    LexicalEnvironment = { ... }, // 词法环境
    VariableEnvironment = { ... }, // 变量环境
}

This Binding(This 绑定)

  • 全局执行上下文中,this 的值指向全局对象,在浏览器中this 的值指向 window对象,而在nodejs中指向这个文件的module对象;
  • 函数执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数;

this的绑定情况情况比较复杂,涉及到动态作用域相关内容。

词法环境(Lexical Environment)(ES6)

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

词法环境的内部组件:

  • 环境记录:存储变量和函数声明的实际位置
  • 对外部环境的引用:可以访问其外部词法环境

词法环境有两种类型

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

使用伪代码表示如下:

GlobalExectionContext = { //全局执行上下文
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Object",
            // 在这里绑定标识符
        },
        outer: <null> // 对外部环境的引用
    }
}
FunctionExectionContext = { // 函数执行上下文
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Declarative",
            // 在这里绑定标识符
        },
        outer: '<Global or outer function environment reference>’// 对外部环境的引用
    }
}

简而言之,在全局环境中,环境记录器是对象环境记录器;在函数环境中,环境记录器是声明式环境记录器。

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

变量环境(var)(ES5)

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

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

比如:

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
    var g = 20;
    return e * f * g;
}
c = multiply(20, 30);

执行上下文如下所示:

GlobalExectionContext = {
    ThisBinding: '<Global Object>',
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 在这里绑定标识符
            a: '< uninitialized >',
            b: '< uninitialized >',
            multiply: '< func >'
        },
        outer: '<null>'
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 在这里绑定标识符
            c: undefined,
        },
        outer: '<null>'
    },
},
    FunctionExectionContext = {
        ThisBinding: '<Global Object>',
        LexicalEnvironment: {
            EnvironmentRecord: {
                Type: "Declarative",
                // 在这里绑定标识符
                Arguments: { 0: 20, 1: 30, length: 2 },
            },
            outer: '<GlobalLexicalEnvironment>'
        },
        VariableEnvironment: {
            EnvironmentRecord: {
                Type: "Declarative",
                // 在这里绑定标识符
                g: undefined
            },
            outer: '<GlobalLexicalEnvironment>'
        },
    }

注意:只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。 这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。 这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。这就是我们说的变量声明提升。

执行阶段

此阶段,完成对所有变量的分配,最后执行代码。主要就是执行变量赋值、代码执行

如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

解释器执行代码的伪逻辑: 1.查找调用函数的代码。 2.执行代码之前,先进入创建上下文阶段:

  • 初始化作用域链
  • 创建变量对象:
    • 创建arguments对象,检查上下文,初始化参数名称和值并创建引用的复制。
    • 扫描上下文的函数声明(而非函数表达式):
    • 为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。
    • 如果函数的名字已经存在,引用指针将被重写。
  • 扫描上下文的变量声明:
    • 为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为undefined
    • 如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。
  • 求出上下文内部“this”的值。 3.激活/代码执行阶段:
  • 在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值

回收阶段

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

举例说明:

这个例子来自极客时间

function foo() {
    var a = 1;
    let b = 2;
    {
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a);
        console.log(b);
    }
    console.log(b);
    console.log(c);
    console.log(d);
}
foo();

编译并创建执行上下文

继续执行代码

图片中忽略了this的绑定和Scope Chain作用域链,重点讨论变量环境和词法环境。

总结

以上就是本文的主要内容,执行上下文中主要通过作用域和作用域链确认环境中相关变量的值或者引用关系,同时通过动态作用域确定this的值。全局环境会创建一个唯一的全局执行上下文,每次预编译过程或者函数执行都会创建一个相关的函数执行上下文。

执行上下文是变量声明、读取、赋值和this的载体,理解执行上下文是理解js运行原理的第一步。