深入理解Javascript中的执行环境(Execution Context)和执行栈(Execution Stack)
作者:小辞 标签:Javascript

如果你想成为一个Javascript开发者,那么你一定要知道Javascript程序的内部运行原理。理解执行环境和执行栈是非常重要的,其有助于理解其他Javascript的概念,比如说提升,作用域和闭包等。

当然,理解执行环境和执行栈的概念也将会使你成为一个更好的Javascript开发者。

闲话少说,马上开始吧。

 

执行环境是什么

简单来说,执行环境就是Javascript代码被计算和执行的环境的一个抽象概念。无论Javascript代码在什么时候运行,它都会运行在 执行环境中。

执行环境的类型

在Javascript中有三种执行环境的类型。

全局执行环境 - 这是一种默认和基础的执行环境。如果代码不在任何的函数中,那么它就是在全局执行环境中。他做了两件事情:首先,它创建了一个全局对象 - windows(如果是浏览器的话),并且把this的值设置到全局对象中。在程序中,只会存在一个全局执行环境。

函数执行环境 - 每次当函数被调用的时候,就会为该函数创建一个全新的执行环境。每个函数都有他们自己的执行环境,但是他们仅仅是在函数被调用的时候才会被创建。其可以有任意多个函数执行环境。无论新的执行环境在什么时候被创建,它都会按照定义的顺序依次执行一系列的步骤,不过这些我们稍后会讲。

eval函数执行环境 - 在eval函数中执行代码也会获得它自己的执行环境,但是eval并不经常被Javascript开发者所使用,所以这里我们目前并不打算讨论它。

 

执行栈

执行栈,在其他编程语言中也被称为调用栈,它是一种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引擎会为这个函数创建一个新的执行环境,并且把它推到当前执行栈的顶部。

当second()函数在first()函数内被调用时,Javascript引擎会为这个函数创建一个新的执行环境,并把它推送到当前执行栈的顶部。当second()函数完成的时候,它的执行环境会从当前的栈中推出去,并且空间会到达当前环境下面的那个执行环境中,也就是first()函数执行环境。

当first()完成以后,它的执行环境会会从堆栈中移出,并且控件会到达全局执行环境。当所有代码执行完以后,Javascript引擎会从当前栈中移出全局执行环境。

 

那么执行环境是如何被创建出来的呢?

到现在为止,我们已经看到Javascript引擎是如何管理执行环境的。那么现在咱们来理解一下执行环境是如何被Javascript引擎创建出来的吧。

执行环境的创建过程分为两个阶段:1,创建阶段,2,执行阶段。

 

创建阶段

执行环境是在创建阶段被创建出来的。在创建阶段会发生下面的事情:

  1. 词法环境组件被创建出来。

  2. 变量环境组件被创建出来。

因此执行环境从概念上可以被表示为:

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

 

词法环境

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

 

环境记录

环境记录是变量和函数声明的地方,其被存储在词法环境内部。

有两种词法环境的类型:

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

对象环境记录 - 全局代码的词法环境包含一个对象环境记录。除了变量和函数声明之外,对象环境记录也会存储全局绑定对象(浏览器中的window对象)。因此对于每个绑定对象的属性(对于浏览器,它包含所有由浏览器给window对象的属性和方法),在记录中创建一个新的条目。

注意 - 对于函数代码,环境记录也会包含参数对象,参数对象包含传递给函数的参数以及索引,和传递给函数的参数的长度(个数)。例如,下面函数的参数对象看起来像这样子的:

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' refers to 'person', because 'calcAge' was called with //'person' object reference
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given

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

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

 

变量环境:

它也是一个词法环境,其环境记录中环境记录保存着在运行环境中的VariableStatements创建的绑定。

正如上面所写的,变量环境也是一个词法环境,因此他有如上定义的词法环境的所有的属性和组件。

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

当遇到函数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在创建阶段定义的变量没有值与他们相关联,但是var定义变量会设置为false。

这是因为,在创建阶段,扫描代码以查找变量和函数声明,当函数定义被全部存储到环境中时,变量首先会被初始化为undefined(在var的情况中),或者保持未初始化状态(在let和const的情况中)。

这就是你在他们定义之前(虽然是undefined)访问var定义的变量,但是当你在定义之前访问let和const定义的变量时,会得到一个引用错误。

这就是我们所谓的提升。

注意 - 在执行阶段,如果javascript引擎在源代码中声明的实际位置找不到let变量的值,那么它将为其分配未定义的值。

 

结论

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

 

觉得不错的话,可以关注微信公众号:干货技术,每周都有一篇前端相关原创干货小文章分享

 

翻译自:

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