JavaScript执行上下文和调用堆栈

如果您是JavaScript开发人员或想成为一名JavaScript开发人员,这意味着您需要了解执行JS代码的内部机制。 尤其是,对于掌握其他JavaScript概念(例如引发变量,作用域和闭包),绝对需要了解执行上下文和调用堆栈。 我们今天发布的翻译资料专门针对JavaScript中的执行上下文和调用堆栈。



执行上下文


简单来说,执行上下文是描述执行JavaScript代码的环境的概念。 代码始终在上下文中执行。

▍运行上下文类型


JavaScript具有三种类型的执行上下文:

  • 全局执行上下文。 这是基本的默认执行上下文。 如果某些代码不在任何函数中,则此代码属于全局上下文。 全局上下文的特征在于存在一个全局对象,对于浏览器,它是window对象,并且this指向该全局对象。 一个程序只能具有一个全局上下文。
  • 函数执行上下文。 每次调用函数时,都会为其创建一个新的上下文。 每个函数都有其自己的执行上下文。 一个程序可能同时具有许多用于执行功能的上下文。 为执行功能创建新的上下文时,它将经历某些步骤序列,我们将在下面讨论。
  • eval函数的执行上下文。 在eval函数内部执行的代码也具有自己的执行上下文。 但是,很少使用eval函数,因此在这里我们将不讨论该执行上下文。

执行栈


执行堆栈,也称为调用堆栈,是LIFO堆栈,用于存储在代码执行期间创建的执行上下文。

当JS引擎开始处理脚本时,引擎将创建一个全局执行上下文并将其放在当前堆栈中。 当检测到调用函数的命令时,引擎将为此函数创建一个新的执行上下文,并将其放置在堆栈的顶部。

引擎执行一个函数,其执行上下文位于堆栈的顶部。 函数完成后,将从堆栈中删除其上下文,并将控制权转移到堆栈中上一个元素中的上下文。

我们将通过以下示例探索这种想法:

 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()函数时,引擎会为此函数创建一个新的上下文并将其放在堆栈的顶部。

当从第first()函数调用first()函数时,将为此函数创建一个新的执行上下文,并且还将其压入堆栈。 second()函数完成其工作之后,将其上下文从堆栈中删除,并将控制权转移到位于其下的堆栈上的执行上下文,即first()函数的上下文。

first()函数退出时,其上下文将从堆栈中弹出,并将控制权转移到全局上下文。 执行完所有代码后,引擎将从当前堆栈中检索全局执行上下文。

关于创建上下文和运行代码


到目前为止,我们讨论了JS引擎如何管理执行上下文。 现在让我们讨论执行上下文是如何创建的,以及它们创建后会发生什么。 特别是,我们正在谈论创建执行上下文的阶段和代码执行的阶段。

execution创建执行上下文的阶段


在执行JavaScript代码之前,将创建执行上下文。 在创建过程中,将执行三个操作:

  1. 确定此值并绑定(此绑定)。
  2. 创建了LexicalEnvironment组件。
  3. VariableEnvironment组件已创建。

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

 ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 

此绑定


在全局执行上下文中, this包含对全局对象的引用(如上所述,在浏览器中,它是一个window对象)。

在函数执行的上下文中,此值取决于函数的调用方式。 如果将其称为对象的方法,则此值绑定到该对象。 在其他情况下, this对象绑定到全局对象或设置为undefined (在严格模式下)。 考虑一个例子:

 let foo = { baz: function() { console.log(this); } } foo.baz();    // 'this'    'foo',    'baz'              //    'foo' let bar = foo.baz; bar();       // 'this'     window,                 //      

词汇环境


根据ES6 规范 ,词法环境是一个术语,用于根据ECMAScript代码的词法嵌套的结构来定义标识符与各个变量和函数之间的关系。 词汇环境由环境记录和对外部词汇环境的引用组成,可以为null

简而言之,词汇环境是一种存储有关标识符和变量的对应关系的信息的结构。 在此,“标识符”是指变量或函数的名称,而“变量”是指对特定对象(包括函数)或原始值的引用。

在词汇环境中,有两个组成部分:

  1. 环境记录。 这是变量和函数声明的存储位置。
  2. 链接到外部环境。 此类链接的存在指示词法环境可以访问父词法环境(作用域)。

有两种类型的词汇环境:

  1. 全局环境(或全局执行上下文)是没有外部环境的词汇环境。 对外部环境的全局环境引用为null 。 在全局环境中(在环境记录中),可以使用与全局对象相关联的内置语言实体(例如ObjectArray等),并且用户还定义了全局变量。 在此环境中, this值指向全局对象。
  2. 函数的环境,在环境记录中存储了用户声明的变量。 对外部环境的引用既可以指示全局对象,也可以指示所讨论功能之外的功能。

有两种类型的环境记录:

  1. 声明性环境记录,用于存储变量,函数和参数。
  2. 环境对象记录,用于在全局上下文中存储有关变量和函数的信息。

结果,在全局环境中,环境记录由对象环境记录表示,而在功能环境中由声明性环境记录表示。

请注意,在函数环境中,环境的声明记录还包含arguments对象,该对象存储索引和传递给函数的参数值之间的对应关系,以及有关此类参数数量的信息。

词汇环境可以表示为以下伪代码:

 GlobalExectionContext = { LexicalEnvironment: {   EnvironmentRecord: {     Type: "Object",     //        }   outer: <null> } } FunctionExectionContext = { LexicalEnvironment: {   EnvironmentRecord: {     Type: "Declarative",     //        }   outer: <        > } } 

环境变量


变量环境也是一个词法环境,其环境记录存储在当前执行上下文中使用VariableStatement命令创建的绑定。

由于变量的环境也是词法环境,因此它具有所有上述词法环境的属性。

在ES6中, LexicalEnvironmentVariableEnvironment组件之间只有一个区别。 它包含以下事实:前者用于存储使用letconst关键字声明的函数和变量的声明,而后者仅用于存储使用var关键字声明的变量绑定。

考虑说明我们刚刚讨论的内容的示例:

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

您可能已经注意到,使用letconst关键字声明的变量和常量没有关联的值,并且使用var关键字声明的变量设置为undefined

之所以如此,是因为在创建上下文期间,代码将搜索变量和函数的声明,而函数的声明则完全存储在环境中。 使用var时,变量的值设置为undefined ,使用letconst时,变量的值保持未初始化。

这就是为什么您可以在声明之前访问用var声明的变量(尽管它们将是undefined ),但是当您尝试访问在声明之前用letconst执行的变量或常量时,会发生错误。

我们刚刚描述的被称为“起重变量”。 在执行为变量声明赋值之前,变量声明会“上升”到其词法范围的顶部。

execution代码执行阶段


这也许是本材料中最简单的部分。 在此阶段,将值分配给变量并执行代码。

请注意,如果在执行代码期间JS引擎在声明的位置找不到使用let关键字声明的变量的值,它将为该变量分配undefined值。

总结


我们刚刚讨论了执行JavaScript代码的内部机制。 尽管要成为一名优秀的JS开发人员,并不一定要了解所有这些知识,但如果您对上述概念有所了解,它将有助于您更好地和更深入地了解该语言的其他机制,例如提高变量,范围,短路。

亲爱的读者们! 您认为除了执行上下文和调用堆栈之外,JavaScript开发人员还知道什么呢?

Source: https://habr.com/ru/post/zh-CN422089/


All Articles