题记:调用栈是 JavaScript 执行代码时的"记忆",它记住了我们从哪里来,也决定了我们将去向何方。
🔍 本节要点
- 调用栈是 JavaScript 运行时管理函数执行顺序的数据结构
- 执行上下文决定"当前代码能看到什么变量"
- 调用栈溢出是 React 开发中真实存在的问题
什么是调用栈?
每当你执行一段 JavaScript 代码,JavaScript 引擎都会创建执行上下文(Execution Context),然后把它压入调用栈(Call Stack)。
调用栈的本质是一个后进先出(LIFO)的栈结构。它记录了:
- 当前正在执行哪个函数
- 执行到哪一行
- 函数的局部变量是什么
- 函数执行完毕后应该返回到哪里
function greet(name) {
return `Hello, ${name}`; // ← 现在在这里
}
function sayGoodbye(name) {
const message = greet(name); // ← 调用 greet
return `${message}, goodbye!`;
}
sayGoodbye('React'); // ← 调用 sayGoodbye
执行顺序对应的栈变化:
初始:[](全局上下文在栈底)
调用 sayGoodbye:
栈: [全局上下文, sayGoodbye上下文]
在 sayGoodbye 内部调用 greet:
栈: [全局上下文, sayGoodbye上下文, greet上下文]
greet 返回后:
栈: [全局上下文, sayGoodbye上下文]
sayGoodbye 返回后:
栈: [全局上下文]
全局执行上下文
JavaScript 代码运行之初,就会有一个全局执行上下文被创建。它负责:
- 创建
window(浏览器)或global(Node.js)对象 - 创建
this,指向全局对象 - 声明全局变量和函数
当浏览器加载脚本时,全局上下文就静静地躺在栈底,等待被函数调用打断。
函数执行上下文里有什么?
每次调用一个函数,一个新的执行上下文就被创建,其中包含:
ExecutionContext {
- VariableEnvironment // let/const 变量和函数声明
- LexicalEnvironment // 同上(ES6+ 两者等价)
- ThisBinding // this 指向谁
- Outer // 外层作用域引用(作用域链)
}
闭包正是利用了 Outer 引用——即使外层函数已经返回、外层上下文已经弹出栈,只要内部函数还保持着对外层变量的引用,外层变量就不会被垃圾回收。
作用域链:变量查找的路径
当代码访问一个变量时,JavaScript 会沿着作用域链向上查找:
const globalVar = '全局变量';
function outer() {
const outerVar = '外层变量';
function inner() {
const innerVar = '内部变量';
console.log(innerVar); // 先找局部
console.log(outerVar); // 沿作用域链向上
console.log(globalVar); // 找到全局
}
inner();
}
作用域链的构建依赖于执行上下文的 Outer 引用。这条链在函数定义时就已经确定,而不是调用时。
调用栈溢出:React 开发中的真实问题
当你递归调用一个函数但没有正确的终止条件时,栈会不断增长,最终溢出:
// 这段代码会导致栈溢出
function deepRecursion() {
deepRecursion(); // 没有终止条件
}
为什么 React 开发者需要关心这个?
React 的组件树是一个递归结构。在 React 16 引入 Fiber 之前, reconciler(协调器)使用的是递归来构建虚拟 DOM 树:
// React 15 的 reconcile(简化)
function reconcile(element) {
if (typeof element.type === 'string') {
// 创建 DOM 节点
return createDOMElement(element);
}
// 递归处理子元素
const children = element.props.children;
children.forEach(child => reconcile(child));
}
这个递归是在调用栈上进行的。当组件树足够深时,可能导致调用栈溢出,并且用户交互无响应——因为栈不空,事件循环就无法处理新的事件。
Fiber 架构的核心改进之一,正是用循环代替递归,把大任务拆分成小单元,每个小单元执行完毕后都能让出主线程,这样就不会撑爆调用栈。
本节小结
调用栈是 JavaScript 执行机制的心脏。理解它,你就理解了:
- 函数是怎么一层层嵌套执行的
- 闭包为什么会"记住"外层变量
- 为什么 Fiber 的循环比递归更适合 React 的渲染模型
下一节我们来看看宏任务与微任务的细节。