第3节:调用栈与执行上下文

16 阅读3分钟

题记:调用栈是 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 引用——即使外层函数已经返回、外层上下文已经弹出栈,只要内部函数还保持着对外层变量的引用,外层变量就不会被垃圾回收。

jimeng-20.jpg

作用域链:变量查找的路径

当代码访问一个变量时,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 的渲染模型

下一节我们来看看宏任务与微任务的细节。

2_1080468923_184_97_3_1098378589_66a27b56910902a8e766d2b25dd3b2db.png