[译]理解Javascript中的执行上下文和执行栈

374 阅读7分钟

原文地址:Understanding Execution Context and Execution Stack in Javascript

执行上下文

执行上下文:执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。JavaScript在运行时都是在执行上下文中运行。

执行上下文的类型

JavaScript 中有三种执行上下文类型。

执行上下文.png

全局执行上下文 —— 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

函数执行上下文 —— 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

怎么创建执行上下文?

怎么创建执行上下文.jpg 我们已经看过 JavaScript 怎样管理执行上下文了,现在让我们了解 JavaScript 引擎是怎样创建执行上下文的。

1)创建阶段和2)执行阶段。

1)创建阶段

执行上下文是在创建阶段创建的。以下是在创建阶段发生的事情:

1.创建了词法环境组件
1.创建了变量环境组件

所以执行上下文在概念上可以表示如下:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

1.1 词法环境(Lexical Environment)

词法环境.jpg 官方ES6文档定义词法环境为:
词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联
一个词汇环境由一个环境记录和一个可能为空的外部词汇环境引用组成。
简单地说,词法环境是一个保存标识符-变量映射的结构。(这里的标识符是变量/函数的名称,变量是对实际对象[包括函数对象和数组对象]或原始值的引用)。 例如,考虑以下代码片段:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}

所以上面代码段的词法环境是这样的:

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

每个词汇环境有三个组成部分:
1.环境记录
2.外部环境的引用
3.this绑定

1.1.1 环境记录

环境记录.jpg

环境记录是变量和函数声明存储在词法环境中的地方

有两种类型的环境记录:

1.声明式环境记录(Declarative environment record)——存储变量和函数声明
2.对象环境记录(Object environment record)——全局代码的词法环境包含一个客观(objective)环境记录。除了变量和函数声明之外,对象环境记录还存储了一个全局绑定对象(浏览器中的window对象)。对于每个绑定对象的属性(在浏览器的情况下,它包含浏览器提供给 window 对象的属性和方法),在记录中创建一个新条目。

注 —对于函数代码,环境记录还包含一个arguments对象,该对象包含传递给函数的索引和参数之间的映射以及传递给函数的参数的长度(数量)。例如,以下函数的参数对象如下所示:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},

1.1.2 外部环境的引用 对外部环境的引用是指它能够接触到外部词汇环境。这意味着如果在当前词法环境中找不到变量,JavaScript引擎可以在外部环境中查找变量。

1.1.2 this绑定 在此组件中,this的值是确定的或设置的。 在全局执行上下文中,this的值引用全局对象。(在浏览器中,这指的是window对象)。

在函数执行上下文中,this的值取决于如何调用函数。如果它被一个对象引用调用,那么this的值被设置为该对象,否则,this的值被设置为全局对象或未定义(严格模式)。例如:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' refers to 'person', because 'calcAge' was called with //'person' object reference
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given

抽象地说,词法环境在伪代码中是这样的:

// 全局执行上下文
GlobalExectionContext = {
  LexicalEnvironment: {     //词法环境
    EnvironmentRecord: {    //环境记录
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,    //外部环境的引用
    this: <global object>   //this指向
  }
}

// 函数执行上下文
FunctionExectionContext = {
  LexicalEnvironment: {     //词法环境
    EnvironmentRecord: {    //环境记录
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,   //外部环境的引用
    this: <depends on how function is called>   //this指向
  }
}

2.变量环境

它也是一个词法环境,它的EnvironmentRecord保存着VariableStatements在这个执行上下文中创建的绑定。

正如上面所写的,变量环境也是一个词汇环境,所以它具有词汇环境的所有属性和组件,如上所定义的。

在ES6中,词法环境组件和变量环境组件的一个区别是,前者用于存储函数声明和变量(let和const)绑定,而后者仅用于存储变量(var)绑定。

2)执行阶段

在这个阶段,所有这些变量的赋值都完成了,代码最终被执行。
让我们看一些例子来理解上面的概念:

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

执行上述代码时,JavaScript引擎会创建一个全局执行上下文来执行全局代码。所以在创建阶段,全局执行上下文看起来是这样的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

在执行阶段,变量分配完成。所以全局执行上下文在执行阶段看起来是这样的。

GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

当遇到对函数multiply(20,30)的调用时,将创建一个新的函数执行上下文来执行函数代码。所以在创建阶段,函数的执行上下文看起来是这样的:

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

在此之后,执行上下文将经历执行阶段,这意味着对函数内部变量的赋值已经完成。所以在执行阶段,函数的执行上下文看起来是这样的:

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函数完成后,返回值存储在c. 所以全局词法环境被更新。之后,全局代码完成,程序结束。

注意——您可能已经注意到,在创建阶段,let和const定义的变量没有任何关联的值,但var定义的变量被设置为undefined 。
这是因为,在创建阶段,代码会扫描变量和函数声明,而函数声明则完整地存储在环境中,变量最初设置为undefined (在情况下var)或保持未初始化(在情况下let 和const)。 这就是为什么您可以var 在声明之前访问已定义的变量(尽管undefined)但在声明之前访问let和const变量时会出现引用错误。 这就是我们所说的提升。

注意——在执行阶段,如果 JavaScript 引擎let在源代码中声明的实际位置找不到变量的值,那么它将分配给它的值undefined。

结论 所以我们已经讨论了 JavaScript 程序是如何在内部执行的。虽然你不需要学习所有这些概念才能成为一个了不起的 JavaScript 开发人员,但对上述概念有一个很好的理解将帮助你更容易、更深入地理解其他概念,如提升、作用域和闭包。