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

352 阅读8分钟

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

前言

如果想成为一名优秀的JavaScript开发者,那么你必须得了解在JavaScript内部是如何执行的,理解执行上下文和执行栈,对于理解其他JavaScript的概念(如变量提升,作用域和闭包)至关重要;

什么是执行上下文

简而言之,执行上下文是评估和执行JavaScript代码的环境的抽象概念。每当JavaScript代码在运行的时候,他都是在执行上下文的内部运行。

执行上下文的类型

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

  • 全局执行上下文——这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的window对象(浏览器的情况下),并且设置this的值等于这个全局变量。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 ——每当一个函数被调用时,都会为该函数创建一个新执行上下文。每个函数都有它自己的执行上下文,不过是在函数被调用是创建的。js内部可以存在任意个函数上下文。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval函数执行上下文——执行在eval函数内部的代码也有属于它自己的执行上下文,但由于JavaScript开发者并不经常使用eval,所以在这里不讨论它。

执行栈

执行栈,也就说在其他编程语言中所说的“调用栈”,是一种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');

img

上述代码的执行上下文栈

当上述代码在浏览器加载时,JavaScript引擎创建了一个全局执行上下文并把它压入当前执行栈。

当遇到first()函数调用时,JavaScript引擎为改函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从**first()函数内部调用second()**函数时,JavaScript引擎为second()函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当second()函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即first()函数的执行上下文。

first()执行完毕,它的执行上下文从当前站弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript引擎从当前栈中移除全局执行上下文。

怎么创建执行上下文

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

创建执行上下文有两个阶段:

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

创建阶段(The Creation Phase)

在JavaScript代码执行前,执行上下文将经理创建阶段。在创建阶段会发生三件事:

  1. this值的绑定,即我们所熟悉的this绑定
  2. 创建**词法环境(LexicalEnvironment)**组件
  3. 创建**变量环境(VariableEnvironment)**组件

所以执行上下文在概念上标识如下:

ExecutionContext = {
    ThisBinding = <this value>,
    LexicalEnvironment = {...},
    VariableEnvironment = {...},
}
This绑定:

在全局执行上下文中,this的值指向全局对象。(在浏览器中,this引用了Winddow对象)

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

let foo = {
    baz: function(){
        console.log(this);
    }
}
foo.baz();	//this引用foo,因为baz被foo对象调用

let bar = foo.baz;
bar();	//this指向全局Window对象,因为没有指定引用对象
词法环境(LexicalEnvironment)

官方的 ES6 文档把词法环境定义为

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

简单来说,词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件: (1)环境记录(EnvironmentRecord)(2)外部环境引用(Outer)

  1. 环境记录器是储存变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其外部词法环境。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是一个没有外部环境的词法环境。全局环境的外部环境引用为null。它拥有一个全局对象(window对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this的值指向这个全局对象。
  • 函数环境,用户在函数中定义的变量被存储在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

注意:对于函数环境而言,环境记录还包含了一个arguments对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度。例如,下面函数的arguments对象如下所示:

function foo(a, b) {  
  var c = a + b;  
}  
foo(2, 3);

// arguments 对象  
Arguments: {0: 2, 1: 3, length: 2},

环境记录同样有两种类型(如下所示):

  • 声明性环境记录 存储变量、函数和参数。一个函数环境包含声明性环境记录。
  • 对象环境记录 用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象记录。

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

GlobalExectionContext = {  
  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  // 标识符绑定在这里 
      outer: <null>  
  }  
}

FunctionExectionContext = {  
  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  // 标识符绑定在这里 
      outer: <Global or outer function environment reference>  
  }  
}

变量环境:

它也是一个词法环境,其EnvironmentRecord包含了有**VariableStatements(变量语句)**在此执行上下文创建的绑定。

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

在ES6中,LexicalEnvironment组件和VariableEnvironment组件的区别在于前者用于存储函数声明和变量(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>  
  }  
}

**注意:**只有在遇到函数multiply的调用时才会创建函数执行上下文。

你可能已经注意到了letconst定义的变量没有任何与之关联的值,但var定义的变量设置为undefined

这是因为在创建阶段,代码会扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为undefined(在var的情况下)或保持未初始化(在letconst的情况下)。

这就是为什么你可以在声明之前访问var定义的变量(尽管是undefined),但如果在声明之前访问letconst定义的变量就会提示引用错误的原因。

这就是我们所谓的变量提升。

执行阶段

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

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

总结

我们已经讨论了 JavaScript 内部是如何执行的。虽然你没有必要学习这些所有的概念从而成为一名出色的 JavaScript 开发人员,但对上述概念的理解将有助于你更轻松、更深入地理解其他概念,如提升、域和闭包等

原文地址:Understanding Execution Context and Execution Stack in Javascript
译文地址:理解 Javascript 执行上下文和执行栈