本文目标:带你从“会用栈”到“理解栈的底层逻辑”。文章不仅讲原理,还有结构图、真实调试示例,以及两道高频面试题(括号匹配 + 最小栈)完整拆解。
🎯 一、为什么要深入理解「栈」?
在前端开发、JS 引擎执行机制、深度算法题里,你会频繁看到:
- 调用栈(Call Stack)
- 执行上下文(Execution Context)
- 事件循环(Event Loop)
- DFS 深度优先
- 括号匹配
- 最小栈、单调栈
- 表达式求值
这些背后都离不开同一个数据结构 —— 栈(Stack) 。
理解栈,等于理解 JS 的一半底层行为。今天我们就来一次真正的“底层级别的栈”解析。
🧱 二、栈是什么?一句话概括
栈(Stack)是一种后进先出(LIFO)的线性结构。
你可以把它理解成一台“只能从一端操作”的自动售货机:
- 入口(Push)
- 出口(Pop)
来看一个结构图:
- push:从上面丢一个新数据上去
- pop:只能从上面拿走一个(永远不能直接拿走中间或底部)
🏗️ 三、JS 的栈底层是什么?数组?链表?硬件?
在 JavaScript 里,栈的底层通常由“连续内存块 + 指针”实现。而在你的代码世界里,最常用的栈是:
✔️ 1. JavaScript 引擎的调用栈(Call Stack)
JS 在执行你的代码时,用栈来维护「函数执行顺序」。
结构图如下:
每次调用函数 → 入栈
函数执行完毕 → 出栈
✔️ 2. 代码实现里的“模拟栈”:直接用数组实现
JavaScript 内置没有 Stack 类型,我们一般用数组来模拟:
const stack = [];
stack.push(1);
stack.push(2);
stack.pop();
但注意:
- push/pop 是 O(1)
- shift/unshift 是 O(n) (不要用)
🔍 四、栈的核心操作(JS 实现 + 原理)
1. push —— 入栈
底层本质:
- 把元素写到连续内存的下一个地址
stack.push(x);
2. pop —— 出栈
底层本质:
- 指针往下移动一格,并返回值
stack.pop();
3. top (peek) —— 查看栈顶,不移除
stack[stack.length - 1]
这三个操作构成栈的全部核心能力。
🌈 五、可视化:栈在 JS 引擎中的真实变化
假设我们执行:
function a(){ b(); }
function b(){ c(); }
function c(){ console.log('hi') }
a();
调用栈变化如下:
Step1: a() 入栈
┌────────┐
│ a() │
└────────┘
Step2: b() 入栈
┌────────┐
│ b() │ ← top
├────────┤
│ a() │
└────────┘
Step3: c() 入栈
┌────────┐
│ c() │ ← top
├────────┤
│ b() │
├────────┤
│ a() │
└────────┘
Step4: c() 出栈 …
理解这一段,对理解 JS 异步、事件循环、Promise 执行顺序至关重要。
⚔️ 六、进入实战!两道最经典「栈」算法题
这两道是前端、算法面试出现率最高的“栈”题。
接下来的内容不仅讲解,还给你完整的 JS 代码拆解 + 图示。
💡 面试题 1:括号匹配(Valid Parentheses)
🧩 核心思想
用一个 map + 栈 来匹配括号。
显示步骤图
s = "{ [ ( ) ] }"
遇左括号:推入期望的右括号
遇右括号:必须与栈顶匹配
图示:
输入:"( [ { } ] )"
栈变化:
push )
push ]
push }
pop } ✔️
pop ] ✔️
pop ) ✔️
最终栈空 = 有效
✔️ 完整代码
const map = {
'(': ')',
'{': '}',
'[': ']'
};
function isValid(s) {
const stack = [];
for (let ch of s) {
if (map[ch]) {
// 左括号,压入它“期望”的右括号
stack.push(map[ch]);
} else {
// 遇到右括号
if (stack.pop() !== ch) {
return false;
}
}
}
return stack.length === 0;
}
📌 时间复杂度
- O(n)
- 只扫一遍
📌 空间复杂度
- 最差 O(n)(“((((((”这种情况)
💡 面试题 2:最小栈 MiniStack(Min Stack)
面试中几乎百分百出现:“请 O(1) 时间返回最小值的栈”。
普通栈没法 O(1) 求最小值,需要遍历:
O(n)
于是我们引入:辅助栈(单调栈) 。
🎨 结构图:两个栈如何配合?
主栈(stack) 辅助栈(stack2:单调递增)
5 ← push 5 5
5 5
3 ← push 3 3
5 5
3
3 ← push 3 3
3
辅助栈始终保持「从栈底 → 栈顶」递减:栈顶永远是最小值。
✔️ 完整 JS 实现
const MiniStack = function () {
this.stack = [];
this.minStack = []; // 单调栈
};
MiniStack.prototype.push = function (x) {
this.stack.push(x);
if (
this.minStack.length === 0 ||
x <= this.minStack[this.minStack.length - 1]
) {
this.minStack.push(x);
}
};
MiniStack.prototype.pop = function () {
const val = this.stack.pop();
if (val === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
return val;
};
MiniStack.prototype.top = function () {
return this.stack[this.stack.length - 1];
};
MiniStack.prototype.getMin = function () {
return this.minStack[this.minStack.length - 1];
};
📌 时间复杂度(全都 O(1))
- push:O(1)
- pop:O(1)
- top:O(1)
- getMin:O(1)
这就是面试官高频问你的原因:
你必须通过栈结构设计,使得逻辑保持 O(1)!
🧭 七、栈的更多应用场景(JS 开发中的真实用例)
| 场景 | 为什么需要栈? |
|---|---|
| 表达式求值 | 经典双栈算法,运算符优先级 |
| 浏览器返回/前进 | 典型的两个栈互转 |
| DFS | 递归其实就是调用栈 |
| React fiber | 递归展开与任务调度 |
| V8 执行引擎 | 栈帧保存执行上下文 |
每当你看到“回溯、嵌套、递归、顺序恢复”,99% 是栈的能力。
🎉 八、全文总结
今天我们从零到一,把 JS 中的栈彻底拆开:
- 栈底层本质:LIFO + 连续内存块
- JS 引擎的调用栈:理解执行机制的核心
- 用数组模拟栈:push/pop = O(1)
- 图示 + 调用流程可视化
- 两道经典的高频面试题:括号匹配、最小栈
- 结构图辅助理解
你现在已经具备——从底层到实战的完整栈思维体系。