JavaScript执行上下文

301 阅读4分钟

执行上下文的定义

执行上下文(Excution Context) 就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念,JavaScript 中运行任何的代码都是执行上下文中运行。

执行上下文栈

是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当代码在浏览器加载时,JavaScript 引擎首先创建一个全局执行上下文并把它压入当前执行栈。而后每执行一个函数时将该函数的函数执行上下文压入执行栈。执行完函数后释放。

执行上下文类型

  • 全局执行上下文:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文:当函数被调用时创建一个函数执行上下文,并压入执行上下文栈,函数执行完毕后从栈中释放该执行上下文。
  • Eval执行上下文:eval 函数内部代码的执行上下文。

执行上下文属性

ES3和ES5对于这部分内容的解释是有差异的:

  • ES3
    • this对象
    • 变量对象(Variable Object)
    • 作用域链(Scope Chain)

变量对象

在全局上下文中变量对象是全局对象。
在函数上下文中用活动对象(activation object, AO)来表示,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活。
以一个例子展现 AO 的变化过程

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

foo(1);
  1. 进入执行上下文,初始化形参、函数声明、变量声明。变量声明和函数表达式会被初始化为undefined。此时的AO:
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
  1. 在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。此时的AO:
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

作用域链

JavaScript 采用词法作用域(Lexical Scoping),也就是静态作用域。
下面以一个例子展现作用域链的变化

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. checkscope 函数被创建,保存作用域链到 内部属性[[scope]]。
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 进入执行上下文阶段,复制函数[[scope]]属性创建作用域链。
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 初始化活动对象,将活动对象压入 checkscope 作用域链顶端。
checkscopeContext = {
    AO: ...
    Scope: [AO, [[Scope]]]
}
  • ES5
    • this对象
    • 词法环境(Lexical Environment)
    • 变量环境(Variable Environment)

词法环境

词法环境是一种持有标识符—变量映射的结构(标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用) 。 词法环境的内部包含两个组件:环境记录器和一个外部环境的引用

  1. 环境记录器:存储函数和变量,函数的词法环境还包含了arguments对象(参数)。
  2. 外部环境的引用:可以访问其父级词法环境,全局上下文的外部环境是null。是个类似作用域链的概念。

变量环境

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

下面来看一个例子,我们用ES5执行上下文的概念解构下面的代码:

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>
  }
}

当函数multiply被调用时,它的执行上下文是这样的:

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>
  }
}

在创建执行上下文阶段时,let和const修饰的变量未被初始化,而var修饰的变量被初始化为undefined。所以在声明之前访问let和const修饰的变量会返回一个引用错误,而访问var修饰的变量不会。这就是变量声明提升。

this对象会单独用一篇文章进行总结。

参考文章
[译] 理解 JavaScript 中的执行上下文和执行栈
JavaScript 执行上下文(ES3版 与 ES5版)
JavaScript深入之变量对象
JavaScript深入之作用域链