【青训营】- JS 的执行上下文

192 阅读6分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

执行上下文是 浏览器在执行JS中的一个环节之一,让我们来分析一下其中的流程。

一、概括

执行上下文,就是只当前正在执行的环境,分为两个阶段:

  • 创建阶段
  • 执行阶段

二、内容

首先我们对JS中的环境进行分类:

  • 全局环境(js代码加载完毕后,进入到预编译也就是进入到全局环境)
  • 函数环境(函数调用的时候,进入到该函数环境,不同的函数,函数环境不同)
  • eval环境(不建议使用,存在安全、性能问题)

(一) 创建过程

创建执行上下文过程分为三部分:

  1. 确定 this的指向(This Binding)
  2. 词法环境(LexicalEnvironment)组件创建
  3. 变量环境(VariableEnvironment)组件创建

1. this指向(This Binding)

  • 全局环境
    • this 指向全局对象,如:浏览器中的this指向windows对象,而在nodeJs中指向module对象
  • 局部(函数)环境
    • this取决于调用函数时的方法,具体由:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数。例如:bind, apply, 箭头函数 这些改变this指向的方法等。

2. 词法环境(LexicalEnvironment)组件创建

词法环境由环境记录对外部环境引入记录两个部分组成。

  • 环境记录:存储当前环境中的变量和函数声明的位置(只是知道当前环境里有什么变量和函数,还没有进行赋值操作),包含了arguments对象。
    • 全局环境记录:记录当前环境下所有的属性,方法的位置。
    • 函数环境记录:包含了函数内定义的所有的属性和方法外,还包含了一个 arguments对象。
  • 外部环境引入记录:存储可以通过自身环境访问到的其他的环境,有点作用域链的感觉。
    • 全局外部环境引入:外部环境引入为null,因为本身就是最外层环境。
    • 函数外部环境引入:可以是 全局环境、也可以是其他函数环境。
GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {        // 环境记录
      Type: "Object",              // 全局环境
      // 标识符绑定在这里 
      outer: <null>                // 对外部环境的引用
  }  
}

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

3. 变量环境(VariableEnvironment)组件创建

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( letconst 绑定,而后者仅用于存储变量( var 绑定。

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

变量提升:
在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 letconst 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

(二) 执行过程

每个执行上下文里,都有三个重要的属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象(Variable object,VO)

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
不同执行上下文下的变量对象稍有不同。

  • 全局上下文的变量对象是全局对象
  • 函数上下文中,我们用 活动对象(activation object, AO) 表示变量对象,两者是一个东西,但也有区别:
    • 变量对象(VO) 是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
    • 当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。

活动对象是在 进入函数上下文时被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行过程可以分为两部分:

  1. 进入执行上下文(分析)
  2. 代码执行(执行)

1. 进入执行上下文

此时还未执行代码,而当前环境的变量对象(或活动对象)包括了:

  1. 函数的所有形参(若当前环境为函数环境的话):由名称和对应值组成一个变量对象的属性与值,如果没有传入实参,则值为 undefined
  2. 函数声明:环境内的所有函数,由名称和对应值(函数对象(function-object)) 组成一个变量对象的属性与值,若变量对象中已存在相同名称的属性则会进行替换。
  3. 变量声明:环境内的所有变量,由名称和对应值(undefined或未定义)组成一个变量对象的属性与值,若变量名称与声明的形参或函数相同,则不会干扰已存在的这类属性。

2. 代码执行

在这个阶段,会从上而下按顺序执行代码,并根据代码,修改变量对象的各个属性的值。

举例子:
一个函数环境

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);

进入执行上下文后,此时的 AO(活动对象)

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

到了代码执行,此时的 AO(活动对象)

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

三、总结

执行上下文的总流程是这样的:

  1. 创建阶段:
    1. 确定 this 的指向 (This Binding)
    2. 词法环境(LexicalEnvironment)组件被创建
    3. 变量环境(VariableEnvironment)组件被创建

总的来说就是初始化变量对象(Variable Object)

确定 this 的指向
初始化内置对象(函数环境为arguments,全局环境为Date、Math等内置对象)
查找当前环境所有形参(函数环境)
查找当前环境所有的函数声明
查找当前环境所有的变量声明
查找当前环境所有的外部引用(作用域链)

  1. 执行阶段
    1. 对变量对象中的属性进行赋值
    2. 对变量对象中的方法进行函数引用
    3. 执行代码

即获取环境中所有变量和函数的值后赋值到变量对象(或活动对象)里,然后执行代码


有什么问题希望大家可以在评论区指出,我及时纠正。

新人上路,还请多多包含。
我是MoonLight,一个初出茅庐的小前端。