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

155 阅读9分钟

原文链接:blog.bitsrc.io/understandi…

如果你想成为一名JavaScript开发人员,那么你必须知道JavaScript内部的执行机制。理解执行上下文和执行堆栈对于理解其他JavaScript概念(如提升、作用域和闭包)至关重要。

正确理解执行上下文和执行堆栈的概念将使你成为更好的JavaScript开发人员。

所以废话不多说,我们开始吧😃

🧐 什么是执行上下文?

简单地说,执行上下文是评估和执行Javascript代码的环境的一个抽象概念。任何用JavaScript运行的代码都是在执行上下文中运行的。

执行上下文的类型

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

  • 全局执行上下文——这是默认的或基本的执行上下文。不在任何函数内的代码都在全局执行上下文中。它会做两件事:创建一个全局对象window(在浏览器环境中),并将全局对象赋值给this。一个程序中只能有一个全局执行上下文。

  • 函数执行上下文——每次调用一个函数时,都会为该函数创建一个全新的执行上下文。每个函数都有自己的执行上下文,但它是在调用或调用函数时创建的。可以有任意数量的函数执行上下文。每当创建一个新的执行上下文时,它都会按照定义的顺序执行一系列步骤,我将在本文后面讨论这些步骤。

  • Eval函数执行上下文——在eval函数内部执行的代码也有自己的执行上下文,但是由于JavaScript开发人员通常不使用eval,所以这里就不讨论它了。

执行栈

执行堆栈(在其他编程语言中也称为“调用堆栈”)是具有LIFO(后进先出)结构的堆栈,该结构用于存储在代码执行期间创建的所有执行上下文。

当JavaScript引擎第一次遇到您的脚本时,它会创建一个全局执行上下文并将其推送到当前执行堆栈。 每当引擎找到函数调用时,它都会为该函数创建一个新的执行上下文,并将其推入堆栈的顶部。

引擎执行其执行上下文位于堆栈顶部的函数。 完成此功能后,将从堆栈中弹出其执行堆栈,并且控件到达当前堆栈中位于其下方的上下文。

让我们用下面的代码示例来了解这一点:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

以上代码的执行上下文堆栈 以上代码的执行上下文堆栈。

当上述代码加载到浏览器中时,JavaScript引擎将创建一个全局执行上下文并将其推送到当前执行堆栈。 当遇到对first()的调用时,JavaScript引擎会为该函数创建一个新的执行上下文,并将其推入当前执行堆栈的顶部。

当从first()函数中调用second()函数时,JavaScript引擎将为该函数创建一个新的执行上下文,并将其推入当前执行堆栈的顶部。 当second()函数完成时,其执行上下文将从当前堆栈中弹出,并且控件到达其下方的执行上下文,即first()函数执行上下文。

first()完成时,将从堆栈中删除其执行堆栈,并将控制权移至全局执行上下文。 一旦执行完所有代码,JavaScript引擎就会从当前堆栈中删除全局执行上下文。

如何创建执行上下文?

到目前为止,我们已经了解了JavaScript引擎如何管理执行上下文。现在,让我们了解JavaScript引擎如何创建执行上下文。

执行上下文的创建分为两个阶段:1)创建阶段2)执行阶段

创建阶段

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

  1. 创建LexicalEnvironment 词法环境组件。

  2. 创建VariableEnvironment 变量环境组件。

因此,执行上下文可以从概念上表示为:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}
// 翻译(伪代码)
执行上下文 = {
  词法环境 = <词法环境在内存中的引用>,
  变量环境 = <变量环境在内存中的引用>,
}

LexicalEnvironment 词法环境

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绑定

EnvironmentRecord 环境记录

环境记录是词法环境中存放变量和函数声明的位置。

环境记录有两种类型:

  • 声明环境记录 —— 顾名思义,它存储变量和函数声明。功能代码的词法环境包含一个声明性环境记录。

  • 对象环境记录 —— 全局代码的词法环境包含一个对象环境记录。除了变量和函数声明外,对象环境记录还存储全局绑定对象(浏览器中的窗口对象)。因此,对于每个绑定对象的属性(对于浏览器,它包含浏览器提供给窗口对象的属性和方法),将在记录中创建一个新条目。

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

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

外部环境引用

对外部环境的引用意味着它可以访问其外部词汇环境。这意味着,如果在当前词法环境中找不到变量,则JavaScript引擎可以在外部环境中查找变量。

This 绑定

在此组件中,this的值是确定或被设置的。

在全局执行上下文中,this引用全局对象。(在浏览器中,this指向Window对象)。

在函数执行上下文中,this的值取决于函数的调用方式。 如果由对象引用调用它,则this值设置为该对象,否则,this值设置为全局对象或undefined(在严格模式下)。 例如:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' 指向 'person', 因为 'calcAge' 是被 'person' 对象引用调用的
const calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局 window 对象, 因为没有给出任何引用

抽象地讲,词法环境在伪代码中看起来像这样:

// 全局执行上下文
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
GlobalExectionContext = {
   词法环境:{
     环境记录:{
       类型:“对象”,
       //标识符绑定在这里
     }
     外部:<null>,
     this:<全局对象>
   }
}

// 函数执行上下文
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}
FunctionExectionContext = {
   词法环境:{
     环境记录:{
       类型:“声明式”,
       //标识符绑定在这里
     }
     外部:<全局或外部函数环境参考>,
     this:<取决于函数的调用方式>
   }
}

Variable Environment 变量环境:

这也是一个词法环境,其环境记录包含在此执行上下文中由变量声明创建的绑定。

如上所述,变量环境也是词法环境,因此它具有如上定义的词法环境的所有属性和组件。

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

执行阶段

在此阶段,将完成对所有这些变量的分配,并最终执行代码。

示例

让我们看一些例子来理解以上概念:

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

当遇到对函数multiple(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中。因此,全局词法环境已更新。之后,全局代码完成,程序结束。

—— 您可能已经注意到,letconst定义的变量在创建阶段没有任何关联的值,但是var定义的变量设置为undefined

这是因为,在创建阶段,将在代码中扫描变量和函数声明,而将函数声明完整存储在环境中,则变量最初会设置为undefined(在var的情况下)或保持未初始化(在letconst)。

这就是为什么您可以在声明var定义的变量之前访问它们(尽管未定义),但是在访问letconst变量之前声明它们时却获得引用错误的原因。

这就是我们所谓的变量提升。

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

结论

我们讨论了JavaScript程序如何在内部执行。 虽然不必学习所有这些概念就可以成为一名出色的JavaScript开发人员,但是对上述概念有一个不错的了解将有助于您更轻松,更深入地理解其他概念,例如变量提升,作用域和闭包。