Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解 JavaScript 中的执行上下文和执行栈 #9

Open
RicoLiu opened this issue Sep 12, 2018 · 6 comments
Open

理解 JavaScript 中的执行上下文和执行栈 #9

RicoLiu opened this issue Sep 12, 2018 · 6 comments

Comments

@RicoLiu
Copy link
Owner

RicoLiu commented Sep 12, 2018

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

英文原文:
https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

如果你是或者打算成为一名 JavaScript 开发者,那么你必须知道 JavaScript 代码内部是如何执行的。理解执行上下文和执行栈对于理解其他的 JavaScript 概念(如:变量提升、作用域、闭包)是非常重要的。

正确的理解执行上下文和执行栈会让你成为更好的 JavaScript 开发者。

那么,让我们开始吧 :)

什么是执行上下文

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

执行上下文的类别

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

  • 全局的执行上下文---这是一个默认的或者说是一个基本的执行上下文。不在任何函数内部的代码,那么就在全局的执行上下文中。它执行两件事情:1、它会创建一个全局的对象(在浏览器中是 window 对象);2、设置 this 的值为全局的对象。在一个程序中,有且只有一个全局的执行上下文。

  • 函数的执行上下文---每当调用一个函数,都会为该函数创建一个新的执行上下文。每一个函数拥有自己的执行上下文,但是在调用或者调用函数的时候会创建它。函数的执行上下文可以有任意数量。每当一个新的执行上下文被创建,它将会按照定义的顺利执行一系列的步骤,这一点会在稍后的文章中讨论。

  • Eval 函数执行上下文---在 Eval 函数内部的代码被执行时也会拥有它自己的执行上下文,但是对于 JavaScript 开发者来说,Eval 函数并不常用,所以我不会在这讨论它。

执行栈

执行栈,在其他的编程语言中也称为「调用栈」,具有先进后出的数据结构,其作用是用来存储所有在代码执行时创建的执行上下文。

每当 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

每当上面的代码在浏览器中加载,JavaScript 引擎会创建一个全局的执行上下文,并将它压栈到当前的执行堆栈中。当调用 first() 函数是,JavaScript 引擎为该函数创建了一个新的执行上下文并将它压栈到当前的执行堆栈中。

second() 函数在 first() 函数内部被调用时,JavaScript 引擎为该函数创建了一个心得执行上下文,并将它压栈到了当前的执行堆栈中。当 second() 函数运行结束时,它的执行栈将从当前堆栈中弹出,并且执行顺序会到达它下一个的执行上下文,也就是 first() 函数的执行上下文。

first() 函数运行结束时,它的执行堆栈将会从栈中移除,并且执行顺序会到达全局的执行上下文。一旦所有的代码都被执行了,JavaScript 引擎会从当前的堆栈中移除全局的执行上下文。

执行上下文是如何创建的?

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

执行上下文创建分两个步骤:1、创建阶段2、执行阶段

创建阶段

在任何 JavaScript 代码执行之前,执行环境经历了创建阶段,创建阶段包含以下三个事:

  1. this 的值确定,也被称为 This Binding.
  2. Lexical Environment 被创建。
  3. Variable Environment 被创建。

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

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

This Binging:

在全局的执行上下文中,this 的值指向的是全局的对象。(在浏览器中,this 指向的是 window 对象)

在函数的执行上下文中,this 的值取决于函数是如何被调用的。如果它是通过对象引用调用的,那么 this 值指向那个对象,否则 this 值指向全局的对象或者是 undefined (在严格模式下)。例如:

let foo = {
  baz: function() {
  console.log(this);
  }
}
foo.baz();    // 'this' refers to 'foo', because 'baz' was called 
             // with 'foo' object reference
let bar = foo.baz;
bar();       // 'this' refers to the global window object, because
             // no object reference was given

Lexical Environment

官方的 ES6 文档定义如下:

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

简单来说,词法环境是一种包含标识符变量映射的结构。(这里的标识符指的是变量名或函数名,变量指的是实际的对象【包含函数类型对象】或原始值。)

在词法环境中,有两种组件:(1) environment record、(2) reference to the outer environment.

  1. environment record 是一个存储变量和函数申明的地方。
  2. reference to the outer environment 指的是:它能够访问到它作用域上的父级词法环境。

两种类型的词法环境:

  • 全局环境(在全聚德执行上下文中)是一个没有外部环境的词法环境。全局环境的外部环境指向的是 null。它有内置的 Object/Array/等等。此环境记录中的原型函数(与全局对象相关联,即窗口对象)以及任何用户定义的全局变量,其值指的是全局对象。
  • 在函数环境中,用户在函数中定义的变量存储在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的任何外部函数。

两种类型的environment record

  • Declarative environment record 存储变量、函数以及参数。
  • Object environment record 用来定义变量和出现在全局上下文中函数之间的关联

简短点来说:

  • global environment 中,environment recordobject environment record.
  • function environment 中,environment recorddeclarative environment record.

注意 --- 对于function environmentdeclarative environment record包含着一个 arguments 对象,该对象存储传递给函数的索引和参数之间的映射以及传递给函数的参数的长度。

抽象地认为词法环境伪代码如下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>
  }
}

Variable Environment:

它也是一个词法环境,其 EnvironmentRecord 包含由此执行上下文中的 VariableStatements 创建的绑定。

如上文所提到的,变量环境也是一个词法环境,所以它有上文中提到的所有词法环境的属性。

在 ES6 中,LexicalEnvironmentVariableEnvironment 有一点不同,前者是用来存储函数声明和变量绑定(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);

执行上下文如下:

GlobalExectionContext = {
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>
  }
}
FunctionExectionContext = {
 
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

注意 --- 仅在遇到函数 multiply 调用时才会创建函数执行上下文。

你可能注意到了:letconst 定义的变量没有任何关联到他们的值,但是 var 定义的变量被设置为 undefined.

这是因为在创建阶段,进行代码扫描以获取变量和函数声明,函数声明存储在它的整个环境中,但是变量被初始化为 undefined (var 定义的变量),或者保留未初始化(let & const 定义的变量)。

这就是为什么你能访问到 var 定义的未声明的变量(尽管是 undefined), 但是访问 letconst 定义的未声明的变量就会得到错误。

这个就是我们所说的「变量提升」。

执行阶段

这是本篇文章中最简单的部分,在此阶段,完成对所有这些变量的分配,最后执行代码。

注意 --- 在执行阶段,如果 JavaScript 引擎不能够找到在源码中已经被声明过的 let 变量的值,那么它的值就会被赋为 undefined.

结论

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

@Chorer
Copy link

Chorer commented Apr 8, 2019

你好,感谢翻译了这篇文章,我这里有个疑惑可能需要你帮忙解答一下。
我在medium看到了原文,之后在这里和掘金上看到了相关译文,但是这两篇译文都和作者原文的表诉有点不同。
原文讲解执行上下文的创建阶段时,只提到词法环境和变量环境,并没有this绑定,this绑定根据作者的说法,实际上是包括在词法环境中。(词法环境包括:环境记录、外部环境引用、this绑定)。
但是在译文里,我发现执行上下文的创建阶段是:词法环境、变量环境、this绑定。所以想问下你是否有对原文进行了修改?如果没有的话,那就是作者后来重新对这个地方进行了修改,但是我疑惑的是这个改动的依据是什么?

@RicoLiu
Copy link
Owner Author

RicoLiu commented Apr 8, 2019

你好,我刚看了一下原文,的确和我翻译的不一样。可能是作者对原文进行了修改?
我看到评论里有人也提出了这个问题:
image

@hax
Copy link

hax commented Apr 8, 2019

@Chorer

我疑惑的是这个改动的依据是什么?

https://es5.github.io/#x10.2
http://www.ecma-international.org/ecma-262/6.0/index.html#table-23

@Chorer
Copy link

Chorer commented Apr 8, 2019

好的,感谢解答。

@stanleyyylau
Copy link

好的,感谢解答。

看了下标准,所以这么说,本文翻译的是正确的,反而原文是错误的?

@stanleyyylau
Copy link

好的,感谢解答。

看了下标准,所以这么说,本文翻译的是正确的,反而原文是错误的?

oh,好吧,两个都看了,原来是 ES5 和 ES6 的区别

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants