JavaScript 执行上下文详解

74 阅读6分钟

执行上下文:JavaScript 代码执行的核心环境

简介

执行上下文可以说是 JavaScript 代码执行的一个环境,存放了代码执行所需的变量、变量查找的作用域链规则以及 this 指向等。它是 JavaScript 很底层的东西,许多问题如变量提升、作用域链和闭包等都可以在执行上下文中找到答案。因此,理解执行上下文对于我们深入学习 JavaScript 至关重要。

执行上下文的分类

执行上下文主要分为三种:

  1. 全局执行上下文:当进入全局代码时会进行编译,在编译中创建全局执行上下文,并生成可执行代码。
  2. 函数执行上下文:执行代码的过程中,如果遇到函数调用,会编译函数内的代码并创建函数执行上下文,生成可执行代码。
  3. eval 执行上下文:当使用 eval 函数时,eval 的代码也会被编译,并创建执行上下文。(基本不使用)

因为执行上下文是在编译阶段创建的,所以我们先了解一下 JavaScript 代码的执行过程。

执行上下文的生命周期

  1. 创建(Creation Phase)

    • 当 JavaScript 引擎开始执行一段代码时,会创建相应的执行上下文。
    • 编译阶段会初始化变量环境和词法环境,并生成可执行代码。
  2. 执行(Execution Phase)

    • 编译阶段完成后,JavaScript 引擎开始逐行执行代码。
    • 在执行过程中,如果遇到函数调用,会创建新的执行上下文并将其压入调用栈。
  3. 销毁(Destruction Phase)

    • 当一个函数执行完毕后,其对应的执行上下文会从调用栈中出栈。
    • 与该执行上下文相关的变量和函数会被销毁,释放内存资源。

示例

function foo() { 
    var a = 1; 
    let b = 2; 
    const c = 3; 
    function inner() { 
    console.log('Inner function'); 
        } 
    } 
foo();

image.png

调用栈

调用栈(Call Stack)是 JavaScript 的执行机制。它是一个后进先出(LIFO)的数据结构,记录了程序中当前正在执行的所有函数调用。调用栈帮助 JavaScript 引擎跟踪函数的调用顺序,确保每个函数在执行完毕后能够正确返回到调用它的函数。

执行代码中的块级作用域

执行代码过程中遇到代码块时,会先将里面通过 letconst 声明的变量存放在词法环境中并设置为初始化。词法环境内部维护了一个小型的栈结构,栈底是函数最外层的变量,每遇到一个代码块,就将所包含的变量压入词法环境的栈结构,代码块执行结束后,将包含的变量弹出,销毁。

function foo() {
  var a = 1;
  let b = 2;
  {
    let b = 3;
    var c = 4;
    let d = 5;
    console.log(a);  // 1
    console.log(b);  // 3
  }
  console.log(b);  // 2
  console.log(c);  // 4
  console.log(d);  // ReferenceError: d is not defined
}
foo();

这里为什么 console.log(d); 打印的是ReferenceError: d is not defined。如下图解释:

image.png

单个执行上下文中变量的查找规则

沿着词法环境的栈顶向下查询,如果在词法环境的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有找到,就继续在变量环境中查找。

函数执行上下文

函数执行上下文里面outer指针的理解: 在 JavaScript 中,每个执行上下文都有一个 outer 指针,这个指针指向当前执行上下文的外部(父级)执行上下文。outer 指针是词法环境(Lexical Environment)的一部分,用于实现词法作用域和闭包。

词法环境(Lexical Environment)

词法环境是一个对象,包含两个主要部分:

  1. 环境记录(Environment Record)

    • 存储变量和函数的绑定。
  2. outer 指针

    • 指向当前词法环境的外部词法环境。

outer 指针的作用

  1. 变量查找

    • 当在一个执行上下文中查找变量时,如果当前词法环境中没有找到该变量,JavaScript 引擎会沿着 outer 指针向上查找,直到找到变量或到达全局词法环境。
  2. 闭包

    • 闭包是指一个函数可以访问其外部作用域中的变量。outer 指针使得内部函数可以访问外部函数的变量。

示例

function bar(){
    console.log(myname);// 打印lisi
}
function foo(){
    var myname = "Alies";  
    bar()
    console.log(myname);// 打印Alies
}
var myname = "lisi";
foo();

image.png

变量提升

变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎将变量的声明部分和函数声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值 undefined

  • 变量提升的实现

    • 并不是物理地移动代码的位置,而是在编译阶段被 JavaScript 引擎放入内存中。
    • var变量提升会赋值为 undefined,函数变量名会将整个函数提升。
    • let、const 不存在变量提升。
// hoisting
console.log(a,func);
console.log(b); // 词法环境中的变量/常量, 在声明之前不可以访问。
// ReferenceError: Cannot access 'b' before initialization
// 暂时性死区   TDZ temporary dead zone
var a = 1;

function func(){

}
let b = 2;
b++;// 词法环境里找b

image.png

  • 函数表达式

    • 只会将变量提升,不会将函数体提升。

示例

console.log(add(2, 3)); 
// ReferenceError: Cannot access 'add' before initialization 
const add = function(a, b) {
    return a + b;
};
  • 声明冲突

    • 当有多个相同类型的声明时,最后一个声明会覆盖之前的声明。
    • 当函数声明与变量声明同时出现时,函数声明优先级更高。

作用域

作用域是一套定义函数调用和变量使用的规则,主要有三种作用域:

  1. 全局作用域

    • 对象在代码的任何地方都能访问,其生命周期伴随着页面的生命周期。
  2. 函数作用域

    • 在函数内部定义的变量和函数,只能在内部访问,外部不能访问到。函数执行结束后,函数内部定义的变量会被销毁。
  3. 块级作用域

    • 由代码块包含的代码会形成一个块级作用域(ES6 之前没有),类似于函数作用域。

通过 var 声明的变量没有块级作用域,通过 letconst 声明的变量有块级作用域。

作用域链

在函数中,如果在当前作用域中找不到所需要的变量,就会沿着作用域链往上查找,直到找到为止。

  • 变量查找规则

    • 首先从执行上下文的词法环境中查找。
    • 如果找不到,就在变量环境中查找。
    • 再找不到,就会沿着 outer 指针去外部的执行上下文中查找。
    • outer 指针的具体引用由词法作用域决定。

总结

通过理解执行上下文、变量提升、作用域链和块级作用域等概念,我们可以更好地管理变量和函数的生命周期,避免常见的陷阱,写出更加清晰和高效的 JavaScript 代码。希望这篇文章对你有所帮助