面试官:说说JS中的执行上下文

1,845 阅读7分钟

⭐说说JS的执行上下文

执行上下文是JavaScript代码执行环境的抽象概念,执行上下文可以看作是一个环境,其中包含了当前代码的变量、函数、this 指向等信息。

但要注意执行上下文与作用域的区别:作用域指的是变量、函数和对象在代码中可访问的范围。它定义了变量的可见性和生命周期。作用域规定了在何处以及如何查找变量。

可以说执行上下文是为了实现作用域而存在的一种机制,但它们并不是完全等同的概念。

执行上下文分为三类:

  1. 全局执行上下文:执行一段JS代码前,会先为它创建全局执行上下文,并将this指向全局对象(在浏览器中是window,node环境中是global)全局执行上下文只会被创建一次,随着程序的开始而创建,随着程序的关闭而销毁。
  2. 函数执行上下文:执行遇到函数时,就会为这个函数创建一个函数执行上下文,但这里的this和函数执行上下文并不等同,只是上下文中保存有this信息
  3. Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用

🌰举个栗子:

//这里是全局执行上下文
        var globalProperty = 0
​
function fun1() {
    //这里是fun1的函数执行上下文
        var fun1Property = 1
    
    function fun2() {
        //这里是fun2的函数执行上下文
        var fun2Property = 2
    }
    
    function fun3() {
        //这里是fun3的函数执行上下文
        var fun3Property = 3
    }
}

每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问。

例如上例中:

  • fun1中可以访问全局执行上下文中的globalProperty,而不能访问内部fun2 fun3中的变量
  • 同理,fun2 fun3可以访问外部的fun1Property以及最外层的globalProperty

执行上下文中存了些什么?

在执行上下文被创建时:

  1. 绑定this,普通声明的函数在执行时才能确定绑定的this值,而es6中箭头函数是词法绑定,被创建时就绑定了所在上下文的this
  2. LexicalEnvironment(词法环境) 组件创建
  3. VariableEnvironment(变量环境) 组件创建
//伪代码流程
ExecutionContext = {  
  ThisBinding = <this value>,     // 确定this 
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}

词法环境:

词法环境是一种持有标识符—变量的映射的结构(简单理解为就是保存了我们定义的变量和函数)

词法环境的组成:

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

词法环境有两种类型:

  • 全局环境:没有外部环境引用的词法环境(环境记录器指向空),它里面保存了JS内置对象、用户定义的全局变量/函数,this绑定为全局对象。
  • 函数环境:用户在函数内部定义的变量存储在环境记录器中,外部环境引用的是外部执行上下文。

根据词法环境的类型不同,环境记录器也分为两类:

  • 全局环境中——对象环境记录器:用来定义出现在全局上下文中的变量和函数的关系,当一个变量或函数在对象环境中被定义或声明时,对象环境记录器会使用一个对象来管理这些变量和函数的绑定关系,通过关联的对象,对象环境记录器实现了对变量和函数的访问和操作 🌰举个栗子:

    var obj = {
      x: 10,
      y: function() {
        console.log('Hello');
      }
    };
    ​
    console.log(obj.x); // 输出:10
    obj.y(); // 输出:Hello
    ​
    

    在这个例子中,obj 对象包含了 x 属性和 y 方法。通过对象环境记录器,我们可以通过 obj.x 访问对象的属性值,并通过 obj.y() 调用对象的方法。

  • 函数环境中——声明式环境记录器:存储变量、函数和参数。

词法环境与对象环境记录器在内部机制上有所不同。其中最明显的区别是词法环境使用一个特殊的数据结构——词法环境记录器(Lexical Environment Record),而对象环境记录器则使用一个普通的 JavaScript 对象

//词法环境伪代码
GlobalExectionContext = {        // 全局执行上下文
    LexicalEnvironment: {        // 词法环境
        EnvironmentRecord: {     // 环境记录器:存储变量和函数声明的实际位置
            Type: "Object",      
            // 在这里绑定标识符  
        }
        outer: <null>           // 对外部环境的引用:可以访问其父级词法环境
    }
}
​
FunctionExectionContext = {     // 函数执行上下文
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 在这里绑定标识符
        }
        outer: <Global or outer function environment reference>
    }
}
​

变量环境:

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

在 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 >,   // let、const声明的变量
      b: < uninitialized >,   // let、const声明的变量
      multiply: < func >      // 函数声明
    }
    outer: <null>
  },
  VariableEnvironment: {     // 变量环境
    EnvironmentRecord: {     
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,         // var声明的变量
    }
    outer: <null>
  }
}
​
FunctionExectionContext = {
  ThisBinding: <Global Object>,
  LexicalEnvironment: {         // 词法环境
    EnvironmentRecord: {   
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},   // arguments对象
    },
    outer: <GlobalLexicalEnvironment>
  },
  VariableEnvironment: {        // 变量环境
     EnvironmentRecord: {
       Type: "Declarative",
       // 在这里绑定标识符
       g: undefined            // var声明的变量
     },
     outer: <GlobalLexicalEnvironment>
  }
}
​

在其中,let和const定义的关联值设为了uninitialized,说明为初始化,而var则被初始化为了undefined,这就是let/const和var的本质区别

综上,JS程序执行的全过程大致可以描述为:

程序启动,全局执行上下文被创建,压入调用栈

  1. 创建全局上下文的 词法环境

    1. 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 let 和 const 定义的变量)
    2. 创建 外部环境引用,值为 null
  2. 创建全局上下文的 变量环境

    1. 创建 对象环境记录器,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 外部环境引用,值为 null
  3. 确定 this 值为全局对象(以浏览器为例,就是 window )

函数被调用,函数执行上下文被创建,压入调用栈

  1. 创建函数上下文的 词法环境

    1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 let 和 const 定义的变量)
    2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
  2. 创建函数上下文的 变量环境

    1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
  3. 确定 this 值

进入函数执行上下文的执行阶段:

  1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

参考文章:

彻底搞懂作用域、执行上下文、词法环境 - 掘金 (juejin.cn)

面试官:JavaScript中执行上下文和执行栈是什么? | web前端面试 - 面试官系列 (vue3js.cn)