JavaScript执行栈和执行上下文详解

59 阅读11分钟

JavaScript 执行栈和执行上下文详解

前言

想象一下,JavaScript 引擎就像一个超级聪明的管家,它需要管理一个复杂的大宅子(你的代码)。为了不搞混乱,它建立了一套严格的管理制度——这就是执行栈(Call Stack / Execution Stack)和执行上下文(Execution Context)。

本文将用最通俗易懂的方式,帮你彻底理解这两个核心概念。

1. 执行上下文(Execution Context):代码的"房间"

什么是执行上下文?

执行上下文(Execution Context)就像是代码执行的"房间"。每当 JavaScript 要执行一段代码时,它就会准备一个专门的房间,房间里放着执行这段代码所需要的所有东西。

🏠 执行上下文就像一个房间
┌─────────────────────────┐
│     函数foo的房间        │
│  ┌─────┐ ┌─────┐ ┌─────┐│
│  │变量 │ │函数 │ │this ││
│  │仓库 │ │仓库 │ │指针 ││
│  └─────┘ └─────┘ └─────┘│
└─────────────────────────┘

执行上下文的类型

JavaScript 中有三种"房间":

  1. 全局房间(全局执行上下文 - Global Execution Context)

    • 🏡 就像大宅子的主厅,程序一启动就准备好
    • 整个程序只有一个
    • 里面放着全局变量和函数
  2. 函数房间(函数执行上下文 - Function Execution Context)

    • 🚪 每次调用函数都会新建一个房间
    • 可以有很多个
    • 函数执行完毕就销毁房间
  3. eval 房间(eval 执行上下文 - Eval Execution Context)

    • 🚫 不推荐使用,就像危险的地下室

房间里都有什么?

每个执行上下文"房间"里都有三个重要的仓库:

执行上下文 = {
  变量环境(VariableEnvironment): {
    // 存放 var 声明的变量和 function 声明的函数
    // 就像一个"传统仓库",东西一进来就有标签
  },
  词法环境(LexicalEnvironment): {
    // 存放 let 和 const 声明的变量
    // 就像一个"现代仓库",需要正式启用才能使用
  },
  this指针(this binding): {
    // 告诉你"这个房间属于谁"
  }
}

2. 执行栈(Call Stack / Execution Stack):房间的管理员

什么是栈?

栈(Stack)就像餐厅里叠盘子的架子:

🍽️ 叠盘子的比喻

    最后放的盘子 ← 最先拿走 (栈顶 - Top)
    ┌─────────┐
    │  盘子3  │
    ├─────────┤
    │  盘子2  │
    ├─────────┤
    │  盘子1  │ ← 最先放的,最后拿走 (栈底 - Bottom)
    └─────────┘
     架子底部

栈的规则很简单:

  • 📥 入栈(Push):只能从顶部放入新盘子
  • 📤 出栈(Pop):只能从顶部拿走盘子
  • 🔄 LIFO 原则(Last In First Out):后进先出

JavaScript 执行栈:房间的电梯

JavaScript 执行栈就像一个只能向上的电梯,管理着所有的"房间"(执行上下文):

🏢 执行栈就像一座楼

楼层4: [ foo函数房间 ] ← 当前正在这里
楼层3: [ bar函数房间 ]
楼层2: [ baz函数房间 ]
楼层1: [ 全局房间 ]     ← 永远在最底层
     ──────────────
     JavaScript引擎

关键规则:

  • 🎯 同一时间只能在一个房间里工作(栈顶)
  • 🏠 全局房间永远在最底层
  • 📞 函数调用 = 新房间进入电梯
  • ✅ 函数结束 = 房间离开电梯

3. 执行上下文的生命周期(Execution Context Lifecycle):房间的装修过程

每个"房间"的准备分为两个阶段,就像装修房子一样:

第一阶段:装修阶段(创建阶段 - Creation Phase)

在这个阶段,JavaScript 引擎是个勤劳的装修工,按照固定的顺序装修房间:

🔨 装修房间的步骤

1️⃣ 准备房间框架(创建变量对象VO - Variable Object)
   ┌─────────────────┐
   │   空房间准备好   │
   └─────────────────┘

2️⃣ 安装门牌号(处理函数参数 - Function Parameters)
   如果是函数房间,在门上写上参数名和值
   参数1: x = 传入的值
   参数2: y = 传入的值

3️⃣ 安装传话筒(创建arguments对象 - Arguments Object)
   只有函数房间有,记录所有传入的参数
   arguments = {0: 值1, 1: 值2, length: 2}

4️⃣ 安装功能区(处理函数声明 - Function Declarationsfunction声明的函数 → 立即安装完整的功能
   就像安装了一台完整的机器

5️⃣ 贴标签(处理变量声明 - Variable Declarationsvar声明的变量 → 先贴个"undefined"标签
   let/const声明的变量 → 贴个"请勿使用"标签

🎯 装修阶段的关键点:

// 装修完成后的房间布局
房间 = {
  传统仓库: {
    // 1. 参数区(只有函数房间有)
    参数x: 实际传入的值,
    参数y: 实际传入的值,

    // 2. 通讯设备(只有函数房间有)
    arguments: {所有传入参数的记录},

    // 3. 完整设备区
    function声明的函数: 完整的函数对象, // ✅ 立即可用

    // 4. 占位区
    var声明的变量: undefined, // ⚠️ 只是占位
  },
  现代仓库: {
    let声明的变量: <请勿使用>, // 🚫 暂时性死区
    const声明的变量: <请勿使用>, // 🚫 暂时性死区
  }
}

💡 记忆口诀:

"参数 arguments 函数 var,let 和 const 要等等"

第二阶段:入住阶段(执行阶段 - Execution Phase)

装修完成后,就开始真正使用房间了:

🏠 入住阶段的变化

装修时:
┌─────────────────┐
│ var a = undefined │  ← 只是占位
│ let b = <请勿使用> │  ← 不能碰
│ function foo(){} │  ← 已经可用
└─────────────────┘

入住后:
┌─────────────────┐
│ var a = 10      │  ← 真正赋值了!
│ let b = 20      │  ← 现在可以用了!
│ function foo(){} │  ← 保持不变
└─────────────────┘

🔄 执行阶段做什么:

  • 📝 逐行执行代码
  • 🏷️ 给变量贴上真正的值
  • 📞 调用函数(创建新房间)

4. 生动实例:一次完整的房间管理

让我们通过一个生动的例子,看看 JavaScript 管家是如何工作的:

// 👇 下面是我们要分析的代码
console.log('程序开始啦!')

var a = 10
let b = 20
const c = 30

function foo(x, y) {
  console.log('进入foo房间')
  console.log(arguments) // 查看传话筒记录

  var m = 100
  let n = 200

  function bar() {
    console.log('进入bar房间')
    console.log(x, y, m, n) // 可以看到外面房间的东西
  }

  var bar2 = function () {
    console.log('这是个后装的设备')
  }

  bar() // 创建bar房间
  console.log('离开foo房间')
}

foo(1, 2, 3) // 创建foo房间,传入3个参数
console.log('程序结束')

🏠 全局房间的装修过程

装修阶段(创建阶段):

全局房间 = {
  传统仓库: {
    a: undefined,              // var变量,先占位
    foo: function foo(x,y){...} // 函数声明,立即安装
  },
  现代仓库: {
    b: <请勿使用>,             // let,暂时性死区
    c: <请勿使用>              // const,暂时性死区
  }
}

入住阶段(执行阶段):

全局房间 = {
  传统仓库: {
    a: 10,                     // 执行 a = 10 后
    foo: function foo(x,y){...} // 保持不变
  },
  现代仓库: {
    b: 20,                     // 执行 let b = 20 后
    c: 30                      // 执行 const c = 30 后
  }
}

🚪 foo 函数房间的装修过程

当调用 foo(1, 2, 3) 时,管家开始装修 foo 房间:

装修阶段(创建阶段):

foo房间 = {
  传统仓库: {
    // 1️⃣ 安装门牌号(处理参数)
    x: 1,                    // 第1个参数
    y: 2,                    // 第2个参数

    // 2️⃣ 安装传话筒(arguments对象)
    arguments: {
      0: 1,                  // 第1个参数
      1: 2,                  // 第2个参数
      2: 3,                  // 第3个参数(多出来的)
      length: 3              // 总共3个参数
    },

    // 3️⃣ 安装功能设备(函数声明)
    bar: function bar(){...}, // 立即安装完成

    // 4️⃣ 贴占位标签(变量声明)
    m: undefined,            // var变量占位
    bar2: undefined,         // 函数表达式也是var,所以只占位
  },
  现代仓库: {
    n: <请勿使用>             // let变量,暂时性死区
  }
}

入住阶段(执行阶段):

foo房间 = {
  传统仓库: {
    x: 1,                    // 参数保持不变
    y: 2,                    // 参数保持不变
    arguments: {0:1, 1:2, 2:3, length:3}, // 保持不变
    bar: function bar(){...}, // 函数声明保持不变
    m: 100,                  // 执行 var m = 100 后
    bar2: function(){...},   // 执行函数表达式赋值后
  },
  现代仓库: {
    n: 200                   // 执行 let n = 200 后
  }
}

5. 执行栈的运作:电梯的上上下下

让我们看看执行栈这个"电梯"是如何运作的:

📈 执行栈的变化过程

第1步:程序启动
┌─────────────────┐
│   全局房间      │ ← 管家在这里工作
└─────────────────┘

第2步:调用 foo(1,2,3)
┌─────────────────┐
│   foo房间       │ ← 管家跑到这里工作
├─────────────────┤
│   全局房间      │ ← 在下面等待
└─────────────────┘

第3步:在foo中调用 bar()
┌─────────────────┐
│   bar房间       │ ← 管家现在在这里
├─────────────────┤
│   foo房间       │ ← 等待中
├─────────────────┤
│   全局房间      │ ← 在最下面等待
└─────────────────┘

第4步:bar执行完毕
┌─────────────────┐
│   foo房间       │ ← 管家回到这里继续
├─────────────────┤
│   全局房间      │ ← 在下面等待
└─────────────────┘
💨 bar房间被销毁了

第5步:foo执行完毕
┌─────────────────┐
│   全局房间      │ ← 管家回到全局房间
└─────────────────┘
💨 foo房间也被销毁了

🔑 关键理解:

  • 🎯 管家同时只能在一个房间工作(栈顶)
  • 📞 函数调用 = 新房间入栈
  • ✅ 函数返回 = 房间出栈并销毁
  • 🏠 全局房间永远在最底层

6. 变量提升(Variable Hoisting)的真相:装修工的提前规划

现在你知道了房间装修的过程,就能理解变量提升的真相了!

🔍 现象观察

// 👀 看起来很奇怪的现象
console.log(a) // undefined(没报错!)
console.log(b) // 报错:Cannot access 'b' before initialization
console.log(foo) // 输出:function foo(){...}(完整函数!)
console.log(bar) // undefined(不是函数!)

var a = 5
let b = 10
function foo() {
  return 'function declaration'
}
var bar = function () {
  return 'function expression'
}

🔨 真相解释

其实是装修工(JavaScript 引擎)提前做了规划:

🏗️ 装修阶段的提前规划

装修工扫描代码:
"我看到了 var alet bfunction foovar bar"

装修工的安排:
┌─────────────────────────────┐
│        传统仓库区域          │
│  a: undefined    (占位)     │
│  foo: function   (完整安装)  │
│  bar: undefined  (占位)     │
└─────────────────────────────┘
┌─────────────────────────────┐
│        现代仓库区域          │
│  b: <请勿使用>   (未启用)    │
└─────────────────────────────┘

💡 这就是为什么:

  • var a → 装修阶段占位为 undefined,所以能访问但值是 undefined
  • let b → 装修阶段未启用,所以不能访问
  • function foo → 装修阶段完整安装,所以能正常使用
  • var bar → 装修阶段只占位,函数表达式要等入住后才安装

🎯 记忆技巧

装修工的口诀:

"函数声明我先装,var 变量占个位,
let 和 const 要等等,表达式后来装"

7. 面试攻略:高频问题解答

🎯 核心概念速记

📚 知识卡片

执行上下文(Execution Context) = 代码执行的房间
执行栈(Call Stack/Execution Stack) = 管理房间的电梯
变量对象(Variable Object) = 房间里的仓库
生命周期(Lifecycle) = 装修阶段(Creation Phase) + 入住阶段(Execution Phase)

🔥 高频面试题

Q1: 什么是变量提升(Variable Hoisting)?

标准答案: 变量提升(Variable Hoisting)是执行上下文创建阶段的表现。就像装修工提前规划房间布局一样,JavaScript 引擎会按照"参数 → arguments → 函数声明 → 变量声明"的顺序预处理代码。函数声明会被完整"安装",var 变量只是"占位"为 undefined。

Q2: let/const 为什么有暂时性死区(Temporal Dead Zone)?

标准答案: let/const 就像需要"预约启用"的现代设备,在装修阶段被标记为"请勿使用"状态,只有执行到声明语句时才正式启用。这个"请勿使用"期间就是暂时性死区(Temporal Dead Zone,简称 TDZ)。

Q3: 函数调用时发生了什么?

标准答案: 就像新建一个房间的完整流程:

  1. 创建新房间(函数执行上下文)
  2. 装修房间(创建阶段:处理参数、arguments、函数声明、变量声明)
  3. 房间入栈(推入执行栈顶)
  4. 入住使用(执行阶段:逐行执行代码)
  5. 房间出栈并销毁(函数执行完毕)

Q4: arguments 对象有什么特点?

标准答案: arguments 就像函数房间的"传话筒",记录了所有传入的参数:

  • 包含所有实参(即使超过形参数量)
  • 有 length 属性表示参数个数
  • 与形参建立映射关系(非严格模式)
  • 只存在于函数执行上下文中

Q5: 函数声明(Function Declaration)和函数表达式(Function Expression)的区别?

标准答案:

  • 函数声明(Function Declaration):装修阶段就完整安装,可以在声明前调用
  • 函数表达式(Function Expression):装修阶段只占位,入住阶段才安装,必须在赋值后调用

Q6: 为什么会栈溢出(Stack Overflow)?

标准答案: 就像电梯超载一样,当递归调用太深或无限递归时,执行栈会堆积太多房间超过最大容量限制,导致栈溢出错误(Stack Overflow Error)。

🧠 记忆宝典

核心记忆点:

  • 执行栈(Call Stack / Execution Stack):后进先出的电梯,管理所有房间
  • 装修阶段(Creation Phase):参数 → arguments → 函数声明 → 变量声明
  • 入住阶段(Execution Phase):逐行执行,变量真正赋值
  • 变量对象(Variable Object):房间的仓库,存储所有标识符
  • var:装修时占位 undefined
  • let/const:装修时暂时性死区(TDZ)
  • 函数声明(Function Declaration):装修时完整安装
  • 函数表达式(Function Expression):装修时占位,入住时安装

结语

通过"房间管理"的比喻,我们彻底理解了 JavaScript 执行栈和执行上下文的工作原理:

  • **执行上下文(Execution Context)**就像代码执行的专用房间
  • **执行栈(Call Stack / Execution Stack)**就像管理房间的电梯系统
  • **变量提升(Variable Hoisting)**就像装修工的提前规划
  • **函数调用(Function Call)**就像新建房间的完整流程

掌握了这些核心概念,你就能轻松理解变量提升、作用域(Scope)、闭包(Closure)等 JavaScript 的高级特性。无论是日常开发还是面试,这些知识都会让你游刃有余!


🎯 最后的建议: 不要死记硬背概念,而要理解背后的"房间管理"逻辑。当你能用自己的话解释给别人听时,你就真正掌握了这些知识!