1.JS 执行上下文和执行栈

  执行上下文就是当前 JS 代码被解析和执行时所在的环境的抽象概念,JS 中运行的代码都是在执行上下文中进行的。

执行上下文的类型

全局执行上下文

  默认的执行上下文,一个程序中只有一个全局执行上下文。不在任何函数中的代码都位于全局执行上下文中。它会:1. 创建一个全局对象,浏览器中这个全局对象就是 window 对象;2. 将 this 指向这个全局对象。

函数执行上下文

  每个函数都有各自的执行上下文,只有在函数被调用时才会被创建,一个程序中可以有任意个函数执行上下文。

Eval 函数执行上下文

  运行在 eval 函数中的代码也有自己的执行上下文,不过用的很少且不建议使用。

执行栈

  执行栈(调用栈),是一个具有 LIFO(后进先出)的结构,用于存储在代码运行期间创建的所有上下文环境。

  JS 引擎首次读取脚本时,会创建一个全局执行上下文环境并压入执行栈中。然后再代码执行过程中,每发生一个函数调用,就会为该函数创建一个新的执行上下文并将其压入栈顶。

  JS 引擎会运行执行上下文在执行栈顶端的函数,当函数运行完后,其对应的执行上下文会从执行栈中弹出,上下文控制权转移到当前执行栈的下一个上下文。

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

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context

执行上下文的创建

  JS 引擎使用执行栈管理执行上下文,下面来看看执行上下文是怎么被创建的。

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

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

  执行上下文在概念上可以这样表示:

ExecutionContext = {  
    ThisBinding = <this value>,  // 确定 this 指向
    LexicalEnvironment = { ... },  	// 词法环境
    VariableEnvironment = { ... },  // 变量环境
}

This Binding

  • 在全局执行上下文中,this 的值指向全局对象。浏览器中,this 的值指向 window 对象;在 nodejs 中 this 的值指向这个文件的 module 对象。

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

词法环境(Lexical Environment)

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

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

  • 环境记录(environment record):存储变量和函数声明的实际位置
  • 对外部环境的引用:用于访问其外部词法环境

词法环境有两种类型:

  • 全局环境:是一个没有外部环境的词法环境。全局环境的外部引用为 null。它拥有一个全局对象及其关联的方法和属性以及任何用户自定义的全局变量,this 的值指向这个全局对象。
  • 函数环境:用户在函数中定义的变量被存储在环境记录中。对外部的引用可以是全局环境也可以是外部函数环境。且环境记录中还包含了一个 arguments 对象,该对象包含了索引和传递给函数的参数之间的映射即参数长度(数量)。

  环境记录同样有两种类型:

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

词法环境在伪代码中看起来像这样:

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

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

变量环境(Variable Environment)

  变量环境也是一个词法环境,因此它具有词法环境的所有属性。

  在 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);

执行上下文为:

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 的调用时才会创建执行上下文。

  在上面的环境记录中,let 和 const 定义的变量没有任何与之关联的值,但 var 定义的变量设置为 undefined。

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

  这就是对于 var 定义的变量能在声明之前就访问(尽管是 undefined),但如果在声明之前就访问 let 和 const 定义的变量就会提示引用错误。

  这就是所谓的变量提升。

执行阶段

  在此阶段,完成对所有变量的分配,最后执行代码。

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


REF:https://juejin.cn/post/6844903704466833421


 目录