深入 JS 之执行上下文

162 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

在上一篇文章中,我们知道了, JS 执行每一段可执行代码的时候, 会做一些准备工作, 也就是创建对应的 执行上下文 (execution context)

每一个执行上下文有三个重要属性:

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

接下来我们分别学习了解一下

变量对象

变量对象是与执行上下文相关的 数据作用域, 存储了上下文中定义的 变量函数声明.

不同的执行上下文的变量对象稍有不同, 其中主要分为 全局上下文的变量对象(GO)函数上下文的变量对象(VO)

全局上下文的变量对象

在第一篇笔记中,我们记录过一个概念, 它就是 全局对象, 而在浏览器端, 全局上下文的变量对象, 就是 全局对象, 也可以理解为我们平常操作的 window, 因为在全局对象中, 会有一个 window 属性指向自身, 所以就会有 window.window.window....

JS 引擎执行代码之前, 就会帮助我们将全局对象创建好, 然后会开始执行我们的其他代码

函数上下文的变量对象

在函数上下文中,我们使用 活动对象(activation object, AO), 来表示变量对象.

活动对象和变量对象其实是同一个对象, 之所以要区分开, 是因为

  1. 在未进入执行阶段之前,变量对象(VO)中的属性都不能访问!或者说就没有属性, 只是开辟了内存空间
  2. 进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

他们处于执行上下文不同生命周期中

活动对象是在进入函数执行上下文的时候,通过函数的 arguments 属性初始化的. argements 也就是 Arguments 对象.

执行过程

执行上下文的代码会被分为两个阶段进行处理, 分析和执行, 我们也可以叫做

  1. 进入执行上下文
  2. 代码执行
1. 进入执行上下文

当进入执行上下文的时候, 这个时候并不会执行代码, 而是会对要执行的代码进行分析, 初始化变量对象

变量对象的初始化包括

  1. 函数的形参
    • 由名称和对应值组成一个变量对象的属性
    • 没有实参则为 undefined
  2. 函数声明
    • 由名称和对应值(函数对象(function-object)) 组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性, 则会完全替换这个属性
  3. 变量声明
    • 由名称和对应值 (undefined) 组成一个变量对象的属性被创建;
    • 如果变量名称和已经生命的形参或函数相同, 则不会干扰已经存在的这类属性

举个例子:

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
}
2.代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

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

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

函数创建

在《深入 JS 之作用域》中, 说了, JS 使用的是静态作用域, 函数的作用域在定义的时候就已经决定了.

这是因为, 在函数有一个内部属性 [[scope]], 在函数创建的时候, 就会保存所有父级变量对象到其中, 你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个例子:

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数激活

当函数激活, 也就是执行的时候, 会进入函数上下文, 在创建 VO/AO 之后, 就会将活动对象添加到作用域链的顶端.

这个时候执行上下文的作用域链,我们命名为 Scope:

Scope = [AO].concat([[Scope]])

至此,作用域链创建完毕.

函数在执行的时候, 查找变量就会在 Scope 中进行查找, 先是顶端的 VO/AO, 如果没找到, 那么就会去上一级的变量对象中查找, 直到我们的全局上下文的变量对象,如果还是没找到,则会报错了

捋一捋

我们通过下面的例子, 做个动图, 来总结一下, 全局执行上下文和函数执行上下文变量对象以及作用域链的创建过程

var name = "global scope";
function foo(){
    var name2 = 'local scope';
    return name2;
}
foo();

execution-context

this

关于 this 比较特殊, 下一篇单独讲解

注意

看了 执行上下文 的具体创建过程了。我们需要注意一点就是 执行上下文 的规范变化

  • 早期执行上下文规范

    Every execution context has associated with it a variable object Variables and functions declared in the source text are added as properties of the variable object. For function code,parameters are added as properties of the variable object.

    每一个执行上下文会被关联到一个变量对象(variable object VO),在源代码中的变量和函数声明会被作为属性添加到 VO 中。 对于函数来说,参数也会被添加到 VO 中。

  • 最新的 ECMA 规范版本中对于执行上下文的描述, 发生少许变更

    Every execution context has an associated Variable Environment. Variables and functions declared in ECMA Script code evaluated in an execution context are added as bindings in that Variable Environment's Environment Record. For function code, parameters are also added as bindings to that Environment Record.

    每一个执行上下文会关联到一个变量环境(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。 对于函数来说,参数也会被作为环境记录添加到变量环境中

  • 通过上面的变化我们可以知道,在最新的 ECMA 标准中,我们前面的 变量对象VO 已经有另外一个称呼了 变量环境VE

虽然只是词汇上的变化, 但是却更加严谨也更加易于实现, 之前必须使用对象来实现, 但是现在可以采用更多的数据结构或者性能更优的实现方式, 如 map 啥的

不过对于我们理解 执行上下文 来说,没有多大影响, 只是引擎实现方面可以更加灵活