(翻译)理解 JavaScript 中的执行上下文和执行栈

154 阅读9分钟

Understanding Execution Context and Execution Stack in Javascript

(理解 JavaScript 中的执行上下文和执行栈)

原文作者: Sukhjinder Arora

原文链接:Understanding Execution Context and Execution Stack in Javascript

0_qPD741uxGb8ldrYt.jpg 照片来自 Greg Rakozy on Unsplash

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

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

废话不多说,让我们开始吧:)

什么是执行上下文

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

执行上下文的类型

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

  • 全局执行上下文:这是默认的或基本的执行上下文,不在任何函数内的代码位于全局执行上下文中。它执行两件事:它创建一个全局的window对象(宿主环境为浏览器的情况下),并将 this 的值设置为全局对象。一个程序中只能有一个全局执行上下文。
  • 函数执行上下文: 每次函数被调用的时候,都会为该函数创建一个新的执行上下文。每个函数都有自己的执行上下文,但是它在函数被调用或回调的时候创建的。一个函数可以有许多函数执行上下文。每当新的执行上下文被创建的时候,它都会按照定义的顺序进行一系列操作,我将在文章的后面讨论这个问题。 -Eval 函数执行上下文: 在 eval 函数中执行的代码也有自己的执行上下文,但由于 JavaScript 开发者通常不使用 eval函数,所以在这里我就不讨论。

执行栈

执行栈,在其他编程语言中也叫做“调用堆栈”,它是一个后进先出 LIFO(Last in, First out)的结构,用于存储代码执行期间创建的所有执行上下文。

当 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');

1_ACtBy8CIepVTOSYcVwZ34Q.webp

上面代码的执行上下文堆栈

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

当 second()函数在 first() 函数中被回调时,JavaScript 引擎会为 second()函数创建新的执行上下文,并且将其推到当前执行栈的栈顶。当 secod() 函数执行结束之后,它的执行上下文从当前栈中弹出,并且控制权来到下面的执行上下文(即first()函数的函数执行上下文)。

当 first() 函数执行结束后,它的执行上下文从当前执行堆中移除,控制权来到全局执行上下文。一旦所有的代码执行完毕, JavaScript 引擎就会从当前堆栈中删除全局执行上下文。

如何创建执行上下文

到目前为止,我们已经了解了 JavaScript 引擎如何管理执行上下文,让我们现在了解一下 JavaScript 引擎如何创建执行上下文。 执行上下文分为两个阶段被创建:

  1. 创建阶段
  2. 执行阶段

创建阶段

执行上下文是在创建阶段被创建的。在创建阶段会发生以下的事情:
创建 Lexical Environment 组件
创建 Variable Environment 组件
因此执行上下文在概念上如下所示:

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

词法环境 (Lexical Environment)

ES6 官方文档将词法环境定义为: 词法环境是一种规范类型,基于 ECMAScript 代码词法嵌套结构用来记录标识符和具体变量或函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境引用组成。

简单来讲,词法环境就是一个保存 标识符-变量映射 的结构,此处标识符是指变量或函数的名称,变量是对实际对象 [包括函数对象和数组对象] 或原始值的引用。

例如,考虑下面的代码片段:

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

上面代码的词法环境如下所示:

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

每个词法环境都有三个部分组成

  1. 环境记录 ( Environment Record)
  2. 外部词法环境的引用
  3. this 绑定

环境记录

环境记录是在词法环境中存储变量和函数声明的地方。通常由两种类型的环境记录。

  • Declarative environment record(声明环境记录)

如它的名字那样,存储变量和函数声明,函数执行上下文的词法环境包含声明 声明环境记录

  • Object environment record (对象环境记录)

全局执行上下文 的词法环境包含一个对象环境记录。除了变量和函数声明以外,对象环境记录还存储了一个全局对象(浏览器中是 window对象)。因此,对于绑定对象的每个属性(浏览器中,它包含浏览器 window 对象上提供的属性和方法),都会在记录中创建一个新条目

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {  //环境记录
      Type: "Object", //对象环境记录
      // Identifier bindings go here
 }
    outer: <null>,
    this: <global object>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {  //环境记录
      Type: "Declarative", // 声明环境记录
      // Identifier bindings go here
 }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

⚠️注意:对于函数代码,环境记录还包括一个 arguments 对象,该对象包含函数的索引,参数,以及函数参数的长度。例如下面函数的 argument 对象所示:

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

外部词法环境的引用

外部词法环境的引用的意思是可以访问函数外部词法环境。这意味着,如果 JavaScript 引擎在当前词法环境中没有找到变量,可以在外部环境中查找变量。

this 绑定

在这个部分, this 的值被确定或设置。 在全局执行上下文中, this 的值 window 对象的引用(在浏览器中)。

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

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
 }
    outer: <null>,
    this: <global object>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
 }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

变量环境 (Variable Environment)

它也是一个词法环境,它的环境记录 (EnvironmentRecord) 保存了在此执行上下文中由变量语句 VariableStatements 创建的绑定。

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

在 ES6 中,词法环境组件(Lexical Environment)和变量环境组件(Variable Environment) 不同的地方是,前者用于存储函数声明和变量(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>
  }
}

当遇到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 定义的变量在创建阶段没有与它们相关的任何值(uninitialized),但是var定义的变量被设置为undefined。

这是因为,在创建阶段,代码会扫描变量和函数声明,而函数声明则完整地存储在环境中,变量最初被设置为未定义(使用 var)或保持未初始化(使用 let 和 const)。

这就是为什么你可以在 var 定义的变量声明之前访问它们(虽然未定义),但在访问 let 和 const 变量声明之前会得到引用错误的原因。

这就是我们所说的提升

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

总结

我们已经讨论了 JavaScript 程序内部是如何执行的。要成为一名出色的JavaScript开发人员,您没有必要学习所有这些概念,但对上述概念有一个良好的理解将有助于您更容易和更深入地理解其他概念,如提升、作用域和闭包。