先别急,我们来看看以下场景
想象一下,你在一个特别小的食堂里打饭。
食堂阿姨贼凶,只从一头上菜。
你端着餐盘走过去,阿姨依次给你放了:
米饭 → 红烧肉 → 青菜 → 煎蛋好家伙,堆了四层。
但你端到座位上准备吃的时候,你第一口吃的是什么?
煎蛋。
因为它就在最上面,最好拿。
而最后一口吃的是米饭,因为它在最底下,被压得最深。
恭喜你,刚才你无意识地用了一次"栈"。
到底什么是栈?
栈(Stack),就是一种只能从一端操作数据的数据结构。
你只能:
往里放东西(叫"入栈",push)
往外拿东西(叫"出栈",pop)
而且有个铁律:后来居上,先走一步。
专业点说叫LIFO:Last In First Out。
说人话:最后进去的,最先出来。
就像叠盘子、子弹上膛、IDE的撤销操作——都是栈的经典应用。
栈的几个重要概念
栈顶(Top):就是那个最上面、最容易被拿走的元素(煎蛋就是栈顶)。
栈底(Bottom):就是那个被压在最底下、最难被拿到的元素(米饭就是栈底)。
入栈(Push):把新元素放到最上面。
Push 五花肉 ← 五花肉(新来的,站在最上面) ← 煎蛋 ← 青菜 ← 红烧肉 ← 米饭(最老的,被压在最底下)出栈(Pop):把最上面的元素拿走。
Pop 一下 ← 青菜 ← 煎蛋被拿走了(走了走了) ← 红烧肉 ← 米饭前端最熟悉的栈:函数调用栈
每天写代码,其实无时无刻不在跟栈打交道。**
当 JavaScript 执行一段代码时,它用调用栈(Call Stack)来记住现在执行到哪了。
看这个例子:
这段代码执行时,调用栈是这么变化的:
第1步:调用 daily → Stack: [daily] 第2步:调用 lunch → Stack: [lunch, daily] 第3步:调用 eat → Stack: [eat, lunch, daily] 第4步:eat 执行完出栈 → Stack: [lunch, daily] 第5步:lunch 执行完 → Stack: [daily] 第6步:daily 执行完 → Stack: []看到了吗?eat 是最后进去的,但它最先出来。
这,就是栈。
栈有什么特点?必须记住这4条
1. 后进先出(LIFO)
这是栈的灵魂。不接受反驳,没有例外。
2. 只有栈顶能操作
栈底的东西想出来?等上面所有人都走了再说。
3. 没有随机访问
普通数组你可以随便拿第5个元素,但栈不行。
你想拿到栈底的米饭?必须先把煎蛋、青菜、红烧肉全部 Pop 出去。
4. 操作只有 O(1)
入栈和出栈都是常量时间,不需要遍历,不需要找来找去,就是快。
前端场景中常用的栈
案例一:JS 引擎的调用栈
上面刚讲过。每次你调一个函数,就入栈;函数执行完,就出栈。
为什么你写递归写多了会报"Maximum call stack size exceeded"?
因为栈太深了,内存装不下了。
// 这种无限递归,调用栈会爆炸 functionrecursion() { recursion(); } recursion(); // Uncaught RangeError: Maximum call stack size exceeded案例二:浏览器历史记录(前进/后退)
浏览器的前进和后退功能,背后就是一个栈。
案例三:撤销操作(Ctrl+Z)
你在 VSCode 里疯狂 Ctrl+Z,为什么能把你的操作一步步撤销回来?
因为编辑器内部维护了一个操作栈。
你做了:打字 → 删掉 → 粘贴 → 撤销 ↓ ↓ ↓ ↓ Stack: [打字] [打字,删掉] [打字,删掉,粘贴] [打字,删掉]
每次撤销就是一次 Pop,栈顶的操作被取消掉。
案例四:括号匹配
写 JS 的时候有没有想过,IDE 怎么知道你写的{[(什么)]}有没有少一个括号?
用栈来判断:
functionisValid(str) { conststack= []; constmap= { ')': '(', ']': '[', '}': '{' }; for (constcharofstr) { if ('([{'.includes(char)) { // 开括号:入栈 stack.push(char); } else { // 闭括号:检查栈顶是不是对应的开括号 if (stack.pop() !==map[char]) { returnfalse; // 不匹配,凉了 } } } // 栈空 = 全部匹配成功 returnstack.length===0; } isValid('{ [ ( ) ] }'); // true isValid('{ [ ( ] ) }'); // false ← 括号乱套了案例五:Vue/React 中的组件渲染栈(简化理解)
React 的协调(Reconciliation)过程,虽然内部用的是链表,但从概念上也可以用栈来理解——子组件的渲染和卸载,本质上遵循后进先出的顺序。
用 JavaScript 五行代码实现一个栈
别被"数据结构"四个字吓到,栈的实现简单到离谱:
classStack { constructor() { this.items= []; // 用数组存,数据不会丢 } push(item) { this.items.push(item); // 入栈 } pop() { returnthis.items.pop(); // 出栈 } peek() { returnthis.items[this.items.length-1]; // 看一眼栈顶是谁 } isEmpty() { returnthis.items.length===0; // 空不空? } } // 用起来 conststack=newStack(); stack.push('米饭'); stack.push('红烧肉'); stack.push('青菜'); stack.push('煎蛋'); stack.pop(); // '煎蛋' ← 最后一个进来的,第一个出来 stack.pop(); // '青菜'栈这个数据结构,看起来简单,但到处都在用。
你写的每个函数调用、每次撤销操作、每个括号校验、浏览器的前进后退——背后都是栈在默默工作。
搞懂了栈,你就算是真正踏进了"程序员的思维世界"。