[JavaScript总结]执行上下文和执行栈

我心飞翔 分类:javascript

什么是执行上下文

简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

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

  • 全局执行上下文:这是默认的,最基础的执行上下文。不在任何函数的中的代码都位于全局执行上下文中。

    它做了两件事:

    1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。
    2. this 指针指向这个全局对象。

    一个程序中只能存在一个全局执行上下文。

  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。

    每个函数都拥有自己的执行上下文,但是只有在函数被调用时才被创建。

    一个程序中可以存在任意数量的函数上下文。

    每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。

  • Eval 函数执行上下文:运行在 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');
 

执行栈

当上述代码在浏览器中加载时,Javascript 引擎会创建一个全局执行上下文并将它推入当前的执行栈。

当调用 first() 函数时, Javascript 引擎为该函数创建了一个新的执行上下文,并将其推到当前执行栈的顶端。

当在first() 函数中调用 second() 函数时,Javascript 引擎为该函数创建了一个新的执行上下文并将其推到当前执行栈的顶端。

second() 函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文,即first() 函数的执行上下文。

first() 函数执行完毕后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到全局执行上下文。

一旦所有代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。

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

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

1.创建阶段

在任意的 JavaScript 代码被执行前,执行上下文处于创建阶段。在创建阶段总共发生了三件事情:

  1. 确定 this 的值,也被称作 This Binding
  2. LexicalEnvironment(词法环境) 组件被创建。
  3. VariableEnvironment(变量环境) 组件被创建。

因此,执行上下文可以在概念上标识如下:

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

This Binding:

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

在函数执行上下文中,this 的值取决于函数的调用方式。

另开一篇

词法环境(Lexical Environment)

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

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

简而言之,词法环境是一个包含 标识符变量映射 的结构。(这里的 标识符 表示变量/函数的名称,变量 是对实际对象【包括函数类型对象】或原始值的引用)

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

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

词法环境 有两种类型:

  • 全局环境 (在全局执行上下文中)是一个没有外部环境的词法环境。

    全局环境的外部环境引用为 null

    它拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量, this 的值指向这个全局变量。

  • 函数环境 用户在函数中定义的变量被存储在 环境记录 中。

    对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

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

  • 声明性环境记录(Declarative Environment Record,DER) 存储变量、函数和参数。一个函数环境包含声明性环境记录。

    • 函数环境记录(Function Environment Record) 也是Declarative Environment Records。用于保存外层 function 的定义,如果不是箭头函数,就给function 提供 this绑定,还提供了super关键字,且包含了arguments对象。
    • 模块环境记录(Module Environment Record) 表示的就是ES6 Module环境中的变量信息。
  • 对象环境记录(Object Environment Record,OER) 用于定义在全局执行上下文中出现的变量和函数的关联。
    全局环境包含对象环境记录。

  • 全局环境记录(Global Environment Record) 表示最外层script标签所包裹的代码环境,这个环境记录项目里面包含了JS内置对象的属性,全局对象的属性以及所有在script中的顶级声明。

函数环境记录

ECMA针对函数环境记录额外添加了一些内部属性,用于辨别不同的种类或不同调用方式的函数:

内部属性 Value 说明 补充
[[ThisValue]] Any 函数内调用this时引用的地址,我们常说的函数this绑定就是给这个内部属性赋值
[[ThisStatus]] "lexical" / "initialized" / "uninitialized" 如果值为"lexical",这是一个箭头函数,没有本地this值。 强行new箭头函数会报错TypeError错误
[[FunctionObject]] Object 在这个对象中有两个属性[[Call]][[Construct]],它们都是函数,如何赋值取决于如何调用函数 正常的函数调用赋值[[Call]],而通过newsuper调用函数则赋值[[Construct]]
[[HomeObject]] Object / undefined 如果该函数(非箭头函数)有super属性(子类),则[[HomeObject]]指向父类构造函数
[[NewTarget]] Object / undefined 如果是通过[[Construct]]方式调用的函数,那么[[NewTarget]]非空 在函数中可以通过new.target读取到这个内部属性。以此来判断函数是否通过new来调用的

[ThisStatus]]全称为[[ThisBindingStatus]]

注意: 对于 函数环境 而言,环境记录 还包含了一个 arguments 对象,该对象包含了索引和传递给函数的参数之间的映射以及传给函数的参数的 长度(数量)

例如,下面函数的 arguments 对象如下所示:

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

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

全局环境记录

字段名 含义
[[ObjectRecord]] Object Environment Record 绑定对象是global object。 它包含全局内置绑定以及FunctionDeclaration, GeneratorDeclaration, and VariableDeclaration在全局代码中绑定 相关联的作用域(realm).
[[GlobalThisValue]] Object 在全局范围内返回的值。 主机(hosts)可以提供任何ECMAScript对象值。
[[DeclarativeRecord]] Declarative Environment Record 包含除了FunctionDeclaration,GeneratorDeclaration和VariableDeclaration绑定之外的关联作用域代码的全局代码中的所有声明的绑定.
[[VarNames]] List of String 由相关作用域的全局代码中的FunctionDeclaration,GeneratorDeclaration和VariableDeclaration声明绑定的字符串名称。

什么时候会创建环境记录

词法环境的创建一般跟以下四种代码相关:

  • global code:全局的源代码文件开始执行script,会初始化一个环境记录。
  • function声明:函数块执行会创建一个新环境记录。
  • blockstatement(yield,await,return):块语句执行也会创建一个新环境记录。
  • try-catch中catch语句:catch执行也会创建环境记录。

注意:环境记录项是在这四种类型代码被执行的时候才会创建

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

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

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

变量环境

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

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

在 ES6 中, LexicalEnviroment 组件和 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 定义的变量就会提示引用错误的原因。

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

2.执行阶段

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

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

参考

【译】理解 Javascript 执行上下文和执行栈

理解JavaScript执行上下文

【JS】详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包|牛气冲天新年征文

回复

我来回复
  • 暂无回复内容