基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

JavaScript 中的执行上下文(Execution Context)和作用域链(Scope Chain)

知识点图片

在 JavaScript 中,执行上下文(Execution Context)作用域链(Scope Chain)是理解 JavaScript 运行机制的两大核心概念。理解它们,就能真正搞懂变量提升(Hoisting)、闭包(Closure)和 this 指向等高级特性。

下面为你系统、深入地拆解这两个概念及其相互关系。


一、 执行上下文(Execution Context)

1. 什么是执行上下文?
执行上下文可以理解为 JavaScript 代码执行时的环境。每当 JavaScript 引擎开始执行一段代码时,它都会先创建一个执行上下文。你可以把它想象成一个“容器”,里面装着代码运行所需的所有信息(变量、函数声明、this 等)。

2. 执行上下文的三种类型:

  • 全局执行上下文(Global Execution Context):默认的、最基础的上下文。代码一开始运行就会创建。它会做两件事:创建一个全局对象(浏览器中是 window),并将 this 指向这个全局对象。一个程序中只有一个全局上下文。
  • 函数执行上下文(Function Execution Context)每当一个函数被调用(执行)时,都会为该函数创建一个全新的执行上下文。每个函数都有自己的上下文。
  • Eval 执行上下文:运行在 eval() 函数中的代码会有自己的上下文(不推荐使用,通常忽略)。

3. 调用栈(Call Stack / Execution Context Stack)
JavaScript 是单线程的,它如何管理这么多执行上下文呢?答案是调用栈

  • 程序启动时,全局上下文被压入栈底。
  • 当调用一个函数时,它的函数上下文被压入栈顶。
  • 栈顶的上下文执行完毕后,会从栈中弹出(出栈),控制权交还给下面的上下文。

4. 执行上下文的生命周期(ES6+ 标准)
分为两个阶段:

  • 创建阶段(Creation Phase):代码执行前发生。
    • 确定 this 的值。
    • 创建词法环境(Lexical Environment):存储 letconst 声明的变量和函数声明。
    • 创建变量环境(Variable Environment):存储 var 声明的变量(这就是变量提升的根源,var 变量在这时被初始化为 undefined)。
  • 执行阶段(Execution Phase)
    • 引擎逐行执行代码,完成变量赋值,执行函数内的语句。

二、 作用域链(Scope Chain)

1. 什么是作用域(Scope)?
作用域是变量和函数的可访问范围。JavaScript 主要有三种作用域:

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6 引入,由 {} 包裹,针对 letconst

2. 词法作用域(Lexical Scoping / 静态作用域)
JavaScript 采用的是词法作用域。这意味着变量的作用域在代码编写(定义)时就已经确定了,而不是在函数调用时确定。一个函数能访问哪些外部变量,取决于这个函数写在哪里

3. 什么是作用域链?
当 JavaScript 引擎在当前作用域中查找一个变量时,如果找不到,它就会向外层(父级)作用域继续查找,直到找到该变量,或者到达全局作用域为止。如果在全局作用域依然找不到,就会报错(ReferenceError)。
这个由内到外、逐层查找的链条,就叫作用域链。

代码示例:

javascript
let a = '全局变量 a';

function outer() {
    let b = '外部函数变量 b';
    
    function inner() {
        let c = '内部函数变量 c';
        console.log(c); // 本地作用域找到 c
        console.log(b); // 本地找不到,去 outer 作用域找到 b
        console.log(a); // 本地和 outer 都找不到,去全局作用域找到 a
    }
    inner();
}
outer();

三、 执行上下文与作用域链的关系

这两个概念是如何联系在一起的?

在 ES6 规范中,执行上下文的词法环境(Lexical Environment)由两部分组成:

  1. 环境记录(Environment Record):实际存放当前上下文中变量和函数声明的地方。
  2. 外部环境引用(Outer Environment Reference)这就是作用域链的物理实现! 它指向当前上下文的父级词法环境。

一句话总结:作用域链是依赖执行上下文中的“外部环境引用”来实现的。

结合两者的完整运行过程:

  1. inner() 函数被调用时,创建了 inner执行上下文并压入调用栈。
  2. inner 上下文的创建阶段,引擎为其记录了本地变量 c,同时赋予它一个外部环境引用(指向 outer 的词法环境,因为 inner 是写在 outer 里面的)。
  3. 在代码执行到 console.log(a) 时,引擎在 inner 的环境记录中找不到 a
  4. 引擎顺着外部环境引用(即作用域链)找到了 outer 的上下文,也没找到 a
  5. 引擎继续顺着 outer外部环境引用找到了全局执行上下文,成功找到了 a

四、 衍生考点:闭包(Closure)

理解了上面两点,闭包就水到渠成了。

什么是闭包?即使创建某个函数的执行上下文已经被销毁(出栈),这个函数依然能通过作用域链访问其外部环境的变量。

javascript
function createCounter() {
    let count = 0; // 这个变量在 createCounter 的执行上下文中
    return function() { // 返回一个内部函数
        count++;
        console.log(count);
    }
}

const counter = createCounter(); // createCounter 执行完毕,其执行上下文从调用栈弹出!
counter(); // 输出 1
counter(); // 输出 2

原理解释:
虽然 createCounter 的执行上下文已经出栈销毁了,但是返回的内部函数由于词法作用域的特性,它的 [[Environment]](内部属性)依然保留着对 createCounter 词法环境的引用。这就是为什么 count 变量没有被垃圾回收机制清除,依然可以被访问和修改的原因。

总结比喻

  • 执行上下文就像是你所在的当前房间(包含了你手边的工具/变量)。
  • 调用栈就是一栋大楼的电梯,记录你从一楼(全局)上到了哪一层(当前执行的函数)。
  • 作用域链就是房间里的一扇单向透明的窗户。如果你在当前房间找不到某个工具,你可以透过窗户看到外面(父级)房间里的工具并借用。如果外面一层也没有,你可以继续往外看,直到看到大楼外(全局)。但外面的人无法看到你房间里的东西。
00:00
00:00