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):存储
let、const声明的变量和函数声明。 - 创建变量环境(Variable Environment):存储
var声明的变量(这就是变量提升的根源,var变量在这时被初始化为undefined)。
- 确定
- 执行阶段(Execution Phase):
- 引擎逐行执行代码,完成变量赋值,执行函数内的语句。
二、 作用域链(Scope Chain)
1. 什么是作用域(Scope)?
作用域是变量和函数的可访问范围。JavaScript 主要有三种作用域:
- 全局作用域
- 函数作用域
- 块级作用域(ES6 引入,由
{}包裹,针对let和const)
2. 词法作用域(Lexical Scoping / 静态作用域)
JavaScript 采用的是词法作用域。这意味着变量的作用域在代码编写(定义)时就已经确定了,而不是在函数调用时确定。一个函数能访问哪些外部变量,取决于这个函数写在哪里。
3. 什么是作用域链?
当 JavaScript 引擎在当前作用域中查找一个变量时,如果找不到,它就会向外层(父级)作用域继续查找,直到找到该变量,或者到达全局作用域为止。如果在全局作用域依然找不到,就会报错(ReferenceError)。
这个由内到外、逐层查找的链条,就叫作用域链。
代码示例:
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)由两部分组成:
- 环境记录(Environment Record):实际存放当前上下文中变量和函数声明的地方。
- 外部环境引用(Outer Environment Reference):这就是作用域链的物理实现! 它指向当前上下文的父级词法环境。
一句话总结:作用域链是依赖执行上下文中的“外部环境引用”来实现的。
结合两者的完整运行过程:
- 当
inner()函数被调用时,创建了inner的执行上下文并压入调用栈。 - 在
inner上下文的创建阶段,引擎为其记录了本地变量c,同时赋予它一个外部环境引用(指向outer的词法环境,因为inner是写在outer里面的)。 - 在代码执行到
console.log(a)时,引擎在inner的环境记录中找不到a。 - 引擎顺着外部环境引用(即作用域链)找到了
outer的上下文,也没找到a。 - 引擎继续顺着
outer的外部环境引用找到了全局执行上下文,成功找到了a。
四、 衍生考点:闭包(Closure)
理解了上面两点,闭包就水到渠成了。
什么是闭包?即使创建某个函数的执行上下文已经被销毁(出栈),这个函数依然能通过作用域链访问其外部环境的变量。
function createCounter() {
let count = 0; // 这个变量在 createCounter 的执行上下文中
return function() { // 返回一个内部函数
count++;
console.log(count);
}
}
const counter = createCounter(); // createCounter 执行完毕,其执行上下文从调用栈弹出!
counter(); // 输出 1
counter(); // 输出 2
原理解释:
虽然 createCounter 的执行上下文已经出栈销毁了,但是返回的内部函数由于词法作用域的特性,它的 [[Environment]](内部属性)依然保留着对 createCounter 词法环境的引用。这就是为什么 count 变量没有被垃圾回收机制清除,依然可以被访问和修改的原因。
总结比喻
- 执行上下文就像是你所在的当前房间(包含了你手边的工具/变量)。
- 调用栈就是一栋大楼的电梯,记录你从一楼(全局)上到了哪一层(当前执行的函数)。
- 作用域链就是房间里的一扇单向透明的窗户。如果你在当前房间找不到某个工具,你可以透过窗户看到外面(父级)房间里的工具并借用。如果外面一层也没有,你可以继续往外看,直到看到大楼外(全局)。但外面的人无法看到你房间里的东西。