javascript-执行上下文

JavaScript 执行上下文

静态作用域与动态作用域

在了解执行上下文执行,需要先了解下 JavaScript 作用域的相关知识
作用域 指的是定义变量的区域,它规定了如何查找变量以及当前执行代码对变量的访问权限问题。
JavaScript 的作用域有两种,全局变量局部变量。函数内部可以直接读取 全局变量,而函数外部一般情况下是无法读取函数内部的 局部变量 的(可以通过闭包实现)。

而 JavaScript 采用的是 词法作用域,即 函数作用域 是在函数定义是决定的,不同与 动态作用域 的在函数调用时才决定。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
var value = 1;

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar() // 1

首先执行 bar 函数,定义了一个变量 value = 2 并且调用了 foo 函数,接着执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,根据函数定义的位置,查找上一层的作用域,找到 value = 1,所以打印的结果是 1(下文中会结合作用域链进行详细分析)

执行上下文

当 JavaScript 执行一段 可执行代码(executable code) 时,会创建对应的 执行上下文(execution context)

在 JavaScript 中可执行代码(executable code)就三种:

  • 全局代码 - 代码首次执行的默认环境
  • 函数代码 - 每当进入一个函数内部时
  • Eval代码 - eval 内部的文本被执行时

下面是一个创建执行上下文的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// global context
var outerValue = 1;

function foo() { // execution context
var innerValue1 = 2;
innerValue2 = 3;

function bar1() { // execution context
return innerValue1;
}

function bar2() { // execution context
return innerValue2;
}

console.log(innerValue1 + innerValue2 + outerValue); // 6
}

foo();
console.log(innerValue1); // Uncaught ReferenceError: innerValue1 is not defined

可以看出:

  • 全局上下文(global context)的变量可以被其它任何上下文访问
  • 可以有任意多个函数上下文(execution context),每次调用函数(包括调用自己)都会创建一个函数上下文和一个私有作用域
  • 函数内部声明的任何变量都不能在当前函数作用域的外部被直接访问

执行上下文堆栈

浏览器里的JavaScript解释器被实现为单线程。即同一时间只能发生一件事情,其他的行文或事件将会被放在叫做执行栈里面排队。
context stack
当浏览器首次载入脚本,会默认进入全局执行上下文,如果在全局执行上下文中调用了一个函数,程序的时序将进入被调用的函数,接着创建一个新的函数执行上下文,并将新建的上下文压入执行栈的顶端,一旦当前上下文执行结束,其将从栈顶弹出,并将上下文控制权交给当前栈顶端的上下文

执行上下文的细节

  • 可将每个执行上下文抽象为有三个属性的对象
1
2
3
4
5
executionContextObj = {
scopeChain: { /* 变量对象(variableObject) + 所有父执行上下文的变量对象 */ },
variableObject: { /* 函数 arguments/参数,内部变量和函数声明 */},
this: {}
}
  • 在函数被调用时,其执行上下文会被创建和激活

    1. 创建阶段(函数被调用,但未执行其中的任何代码之前)
      创建作用域链(scopeChain)
      创建参数,函数和变量
      this的值
    2. 激活阶段(代码执行)
      指派函数的引用和变量的值,执行代码
  • 相关的伪逻辑

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

    • 函数以及调用其的代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function foo(i) {
      var a = 'lugus';
      var b = function changeName() {
      };
      function c() {
      }
      }

      foo(25);
    • 创建阶段

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      fooExecutionContext = {
      scopeChain: { ... },
      variableObject: {
      arguments: {
      0: 25,
      length: 1
      },
      i: 25,
      c: pointer to function c()
      a: undefined,
      b: undefined
      },
      this: { ... }
      }
    • 激活/执行阶段

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      fooExecutionContext = {
      scopeChain: { ... },
      variableObject: {
      arguments: {
      0: 25,
      length: 1
      },
      i: 25,
      c: pointer to function c()
      a: 'lugus',
      b: pointer to function changeName()
      },
      this: { ... }
      }
  • 实际中的一个实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    (function() {
    console.log(typeof foo); // 函数指针
    console.log(typeof bar); // undefined

    var foo = 'hello',
    bar = function() {
    return 'world';
    };

    function foo() {
    return 'hello';
    }

    console.log(typeof bar); // 函数指针
    }());

    从实例中可以看出 foo 在创建时就在 VO 上创建了一个属性 foo 值为 undefined,但由于函数声明 foo 发现 foo 属性已经存在与VO中,foo 的引用的指针被重写,所以第一个打印出的是一个函数指针,而 bar 也在创建时在 VO 上创建了一个属性 bar 值为 undefined,但 var bar = function() { return ‘world’; };这条语句只会在激活/执行阶段指派 bar 的值(因为是函数表达式而不是函数声明,不会再创建阶段更改 bar 的引用),所以第二个打印的是 undefined,而在第三个打印的时候随着代码一行行的执行变量 bar 的值也被指派了,所以打印出函数指针。

作用域链(scope chain)

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

下面就本文中的第一个例子分析下:

1
2
3
4
5
6
7
8
9
10
11
12
var value = 1;

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar() // 1
  1. 执行全局代码,创建全局上下文,全局上下文被压入执行上下文堆栈

    1
    2
    3
    ECStack = [
    globalContext
    ];
  2. 全局上下文初始化,同时 foo 函数与 bar 函数被创建,作用域链被保存到本函数的内部属性[[scope]]中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    globalContext = {
    VO: [global, scope, value, foo, bar],
    scopeChain: [globalContext.VO],
    this: globalContext.VO
    }

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

    bar.[[scope]] = [
    globalContext.VO
    ];
  3. bar函数被调用执行,创建 bar 函数执行上下文,bar 函数执行上下文被压入执行上下文栈

    1
    2
    3
    4
    ECStack = [
    barContext,
    globalContext
    ];
  4. bar 函数执行上下文初始化创建阶段,并复制 bar 函数内部属性[[scope]]创建作用域链

    1
    2
    3
    barContext = {
    Scope: bar.[[scope]]
    }
  5. 然后用 arguments 对象创建活动对象并初始化,还有形参,函数声明,变量声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    barContext = {
    AO = {
    arguments: {
    length: 0
    },
    value: undefined,
    }
    Scope: bar.[[scope]]
    }
  6. 将活动对象压入 bar 作用域链顶端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    barContext = {
    AO = {
    arguments: {
    length: 0
    },
    value: undefined
    }
    Scope: [AO, bar.[[scope]]]
    }
  7. 准备结束,一步步指派AO的属性值,开始执行 bar 函数,调用 foo 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    barContext = {
    AO = {
    arguments: {
    length: 0
    },
    value: 2
    }
    Scope: [AO, foo.[[scope]]]
    }
  8. foo 函数被调用执行,创建 foo 函数执行上下文,foo 函数执行上下文被压入执行上下文栈

    1
    2
    3
    4
    5
    ECStack = [
    fooContext,
    barContext,
    globalContext
    ];
  9. foo 函数执行上下文初始化创建阶段,并复制 foo 函数内部属性[[scope]]创建作用域链

    1
    2
    3
    fooContext = {
    Scope: foo.[[scope]]
    }
  10. 然后用 arguments 对象创建活动对象并初始化,还有形参,函数声明,变量声明

    1
    2
    3
    4
    5
    6
    7
    8
    fooContext = {
    AO = {
    arguments: {
    length: 0
    }
    }
    Scope: foo.[[scope]]
    }
  11. 将活动对象压入 foo 作用域链顶端

    1
    2
    3
    4
    5
    6
    7
    8
    fooContext = {
    AO = {
    arguments: {
    length: 0
    }
    }
    Scope: [AO, foo.[[scope]]]
    }
  12. 准备结束,一步步指派AO的属性值,并开始执行 foo 函数,此时的 Scope 其实是[fooContext.AO, globalContext.VO],value的值在 foo.Context.AO 中没查找到,会去找父级 scopechain 里面的值,即 globalContext.VO,所以值是1而不是2

    1
    2
    3
    4
    5
    6
    7
    8
    fooContext = {
    AO = {
    arguments: {
    length: 0
    }
    }
    Scope: [AO, foo.[[scope]]]
    }

变量对象(VO)与活动对象(AO)的区别

  • 变量对象(Variable Object),每个 JS 的执行上下文中都会有一个对象来存放执行上下文中可被访问但是不能被 delete 的函数声明,形参,变量声明等。他们都会被挂载在这个对象上,但这个对象是规范上或说是引擎上实现的,但不能在 JS 环境中访问到的活动对象

  • 活动对象(Activation Object),每进入一个执行上下文时,这个执行上下文中的变量对象就会被激活,这样该上下文中的函数声明,形参,变量声明就可以被访问,所以说活动对象可以看作是正在被执行和引用的变量对象

参考

冴羽的博客
js 中的活动对象 与 变量对象 什么区别?