ES2018 最新 理解Javascript中的执行上下文和执行栈

蚀刻
• 阅读 594

写在前头

最近看执行上下文,一直没有找到很好的文章或者书籍。大多数都是ES3的旧解释了。

执行上下文在 ES3 中,包含三个部分。

scope:作用域,也常常被叫做作用域链。

variable object:变量对象,用于存储变量的对象。

this value:this 值。

在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。

lexical environment:词法环境,当获取变量时使用。

variable environment:变量环境,当声明变量时使用。

this value:this 值。

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容。

lexical environment:词法环境,当获取变量或者 this 值时使用。

variable environment:变量环境,当声明变量时使用

code evaluation state:用于恢复代码执行位置。

Function:执行的任务是函数时使用,表示正在被执行的函数。

ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。

Realm:使用的基础库和内置对象实例。

Generator:仅生成器上下文有这个属性,表示当前生成器。

下面是我推荐的不同版本的执行上下文的文章

  • ES3

冴羽老师的
JavaScript深入之执行上下文

  • ES5

掘金翻译计划
[[译] 理解 JavaScript 中的执行上下文和执行栈](https://juejin.cn/post/684490...)

  • ES2018

原文地址:Understanding Execution Context and Execution Stack in Javascript (要翻墙) 下面的内容就是我翻译的这篇文章(四级水平加机翻,yyds)

了解JavaScript程序是如何内部执行的

ES2018 最新 理解Javascript中的执行上下文和执行栈

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

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

闲话少说,让我们开始吧:)

什么是执行上下文?

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

执行上下文的类型(Types of Execution Context)

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

  • 全局执行上下文——这是默认的或基本的执行上下文。任何不在函数内部的代码位于全局执行上下文中。它执行两件事:它创建一个全局对象,它是一个window对象(在浏览器的情况下),并将this的值设置为等于全局对象。一个程序中只能有一个全局执行上下文。
  • 函数执行上下文——每次调用函数时,都会为该函数创建一个全新的执行上下文。每个函数都有自己的执行上下文,但它是在调用或调用(原文是it’s created when the function is invoked or called)函数时创建的。可以有任意数量的函数执行上下文。每当创建一个新的执行上下文时,它都会按照已定义的顺序执行一系列步骤,我将在本文后面讨论这些步骤。
  • Eval函数执行上下文——在Eval函数内部执行的代码也会获得它自己的执行上下文,但JavaScript开发人员通常不使用Eval,所以我在这里不讨论它。

执行栈(Execution Stack)

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

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

ES2018 最新 理解Javascript中的执行上下文和执行栈

图片就是上面代码的执行上下文堆栈。

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

当在first()函数中调用second()函数时,Javascript引擎会为该函数创建一个新的执行上下文,并将其推到当前执行栈的顶部。当second()函数结束时,它的执行上下文从当前栈中弹出,控件到达它下面的执行上下文,也就是first()函数的执行上下文。

如何创建执行上下文?

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

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

组件创建阶段(The Creation Phase)

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

  1. 创建LexicalEnvironment组件。
  2. 创建VariableEnvironment组件。

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

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

词法环境(Lexical Environment)

官方ES6文档将词汇环境定义为

(词法环境)Lexical Environment是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由一个环境记录和一个可能为空的外部词汇环境引用组成。

简单地说,词法环境是一个保存标识符-变量映射的结构。(这里标识符指的是变量/函数的名称,变量是对实际对象[包括函数对象和数组对象]或原始数据的引用)。

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

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. Reference to the outer environment(指向外部环境的引用)
  3. This binding. (this绑定)

Environment Record (环境记录器)

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

此外,环境记录器亦有两类:

  • 声明性环境记录(Declarative environment record)——顾名思义,它存储变量和函数声明。函数代码的词法环境包含一个声明性环境记录。
  • 对象环境记录(Object environment record)——全局代码(global code)的词法环境包含一个客观环境记录(objective environment record)。除了变量和函数声明,对象环境记录(the object environment record)还存储了一个全局绑定对象(浏览器中的window对象)。因此,对于每个绑定对象的属性(在浏览器中,它包含浏览器提供给window对象的属性和方法),记录中会创建一个新条目(new entry)。

注意:对于函数代码(function code),环境记录还包含一个参数对象(argument对象),该对象包含传递给函数的索引和参数之间的映射,以及传递给函数的参数的长度(数量)。例如,下面函数的参数对象是这样的:

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

Reference to the Outer Environment(指向外部环境的引用)

Reference to the Outer Environment指的是它能够接触到外部的词法环境。这意味着,如果在当前词法环境中没有找到想要查找的变量,JavaScript引擎可以在外部环境中查找它们。

This Binding (this绑定)

在此组件中,this的值被确定或设置(determined or set)。

在全局执行上下文中,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
// 'this'指的是'person',因为'calcAge'是用'person'对象引用调用的
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given
// 'this'引用全局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>
  }
}

变量环境 (Variable Environment)

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

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

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

执行程序阶段(Execution Phase)

在这个阶段,所有这些变量的赋值都完成了,代码也最终执行了。

Example (例子)

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

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

当执行上述代码时(the above code is executed),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 >
  }
}

在执行阶段(During the execution phase),完成变量赋值。因此,在执行阶段,全局执行上下文将类似于以下内容。

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

当遇到对function 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>
  }
}

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

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

这是因为,在创建阶段,代码被扫描以查找变量和函数声明,而函数声明被完整地存储在环境中,变量最初被设置为未定义(对于var)或保持未初始化(对于letconst)。

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

这就是我们所说的变量提升(hoisting)。

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

总结(Conclusion)

我们已经讨论了JavaScript程序是如何在内部执行的。虽然要成为出色的JavaScript开发人员并不需要学习所有这些概念,但充分理解上述概念将有助于您更容易、更深入地理解其他概念,如变量声明提升(hoisting)、作用域(Scope)和闭包(Closures)。

就是这样,如果你觉得这篇文章有帮助,请点击👏按钮,并随时在下面发表评论!我很乐意和😃交流

本文由mdnice多平台发布

点赞
收藏
评论区
推荐文章
Dax Dax
4年前
JS核心原理理解闭包
前置概念在正式看闭包之前,我们先来学习一下前置知识,那就是JS中的作用域,我们知道,在ES5之中,作用域分为两种:全局作用域和函数作用域,随着ES6的到来,新增了块级作用域,想更好的理解闭包,那么搞清楚作用域是首要条件全局作用域我们知道,对于变量而言,我们一般会分成两类:全局变量和局部变量,一般定义在最外围环境的为全局变量,定义在函数当中的为局部变量,在we
Souleigh ✨ Souleigh ✨
5年前
JS - 作用域
一、作用域作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合换句话说,作用域决定了代码区块中变量和其他资源的可见性举个例子function myFunction(){    let inVariable  "函数内部变量";}myFunction();//要先执行这个函数,否则根本不知
Karen110 Karen110
4年前
一篇文章带你了解JavaScript作用域
在JavaScript中,对象和函数也是变量。在JavaScript中,作用域是你可以访问的变量、对象和函数的集合。JavaScript有函数作用域:这个作用域在函数内变化。一、本地JavaScript变量一个变量声明在JavaScript函数内部,成为函数的局部变量。局部变量有局部作用域:它们只能在函数中访问。JS://codeherecann
Symbol卢 Symbol卢
4年前
秒懂js作用域与作用域链
JavaScript中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,本文我会尽我所能用最简单的方式来解释作用域和作用域链,希望大家有所收获!好了下面开始我们的正文作用域常见的解释(什么是作用域)1.一段程序代码中所用到的名字并不总是有效,而限定它的可用性的范围就是这个名字的作用域;2.作用域规定了
菜园前端 菜园前端
2年前
为你解惑JS作用域和作用域链知识
原文链接:变量作用域一个变量的作用域(scope)是程序源代码中定义这个变量的区域。全局变量拥有全局作用域,在JavaScript代码中的任何地方都是可以访问的。然而在函数内声明的变量只能在函数体内访问,它们是局部变量,作用域是局部性的。函数参数也是局部变
御弟哥哥 御弟哥哥
5年前
彻底理解js的作用域链
在之前的文章(https://www.helloworld.net/p/G4dFV7tALU4J)中我已经介绍了执行上下文的变量对象。在这一篇文章我要介绍执行上下文的作用域链了。执行上下文.作用域链(scopechain)作用域链与变量对象有着密不可分的关系,因为作用域链就是变量对象的数组!其中第
Jacquelyn38 Jacquelyn38
4年前
你所知道的JS变量作用域
变量的作用域,指的是变量在脚本代码中的可读、可写的有效范围,也就是脚本代码中可以使用这个变量的区域。在ES6之前,变量的作用域主要分为全局作用域、局部作用域(也称函数作用域)两种;在ES6及其之后,变量的作用域主要分为全局作用域、局部作用域、块级作用域这3种。相应作用域变量分别称为全局变量、局部变量、块级变量。全局变量声明在所有函数之外;局部变量是在函数体内
御弟哥哥 御弟哥哥
5年前
彻底理解js的执行上下文,以及变量对象
在js中,执行上下文(ExecutionContext)是非常重要的一种对象,它保存着函数执行所需的重要信息,其中有三个属性:变量对象(variableobject),作用域链(scopechain),this指针(thisvalue),它们影响着变量的解析,变量作用域和函数this的指向。上下文栈(ExecutionContextS
Stella981 Stella981
4年前
JavaScript易错知识点整理
前言本文是我学习JavaScript过程中收集与整理的一些易错知识点,将分别从变量作用域,类型比较,this指向,函数参数,闭包问题及对象拷贝与赋值这6个方面进行由浅入深的介绍和讲解,其中也涉及了一些ES6的知识点。JavaScript知识点1.变量作用域vara1;functio
Stella981 Stella981
4年前
Flask的RuntimeError错误处理
在Flask框架中,为我们提供了丰富的上下文对象/变量(request、session、current\_app、g),这些上下文对象可以根据不同的上下文环境具备不同的值,所以他们是依赖于上下文环境的,而他们的使用也必须在上下文环境中,如果脱离了上下文环境,对这些没有上下文环境的上下文对象/变量进行相应的操作就会报错:RuntimeError  通俗一点
Wesley13 Wesley13
4年前
(ES5版)深入理解 JavaScript 执行上下文和执行栈
!(https://oscimg.oschina.net/oscnet/9223e9dacee346f68780d67ac0877f7d.png)译者序最近在研究JavaScript基础性的东西,但是看到对于执行上下文的解释我发现有两种,一种是执行上下文包含:scope(作用域)、variableobj