一、一个问题,两个维度
"用两个栈模拟队列"是经典面试题。但它的价值不止于算法------当代码写出来的时候,JavaScript 的原型机制已经隐含在其中。
这道题的独特之处在于:算法的实现过程同时也是理解语言特性的最佳入口。
二、两个栈模拟队列的核心思路
栈是 FILO,队列是 FIFO,方向正好相反。但两个栈配合就能解决:一个专门接收新元素(输入栈),另一个专门处理出队(输出栈)。
关键操作是"倒数据"------当输出栈为空时,把输入栈的所有元素依次弹出并压入输出栈。由于栈的后进先出特性,倒一次之后,最早进入输入栈的元素就到了输出栈的栈顶。
javascript
const MyQueue = function () {
this.stack1 = []; // 输入栈
this.stack2 = []; // 输出栈
};
MyQueue.prototype.push = function (x) {
this.stack1.push(x);
};
MyQueue.prototype.pop = function () {
if (this.stack2.length === 0) {
while (this.stack1.length > 0) {
this.stack2.push(this.stack1.pop());
}
}
return this.stack2.pop();
};
MyQueue.prototype.peek = function () {
if (this.stack2.length === 0) {
while (this.stack1.length > 0) {
this.stack2.push(this.stack1.pop());
}
}
return this.stack2[this.stack2.length - 1];
};
MyQueue.prototype.empty = function () {
return this.stack1.length === 0 && this.stack2.length === 0;
};
push 永远是 O(1),pop 和 peek 均摊 O(1)------每个元素最多被倒两次。
三、prototype 在这段代码里做了什么
上面用 function + prototype 而非 class 来写。这不是怀旧,而是故意暴露 JavaScript 面向对象的底层机制。
MyQueue.prototype 是一个对象,存放所有实例共享的方法。通过 new MyQueue() 创建的每个实例,内部通过 __proto__ 链接到 MyQueue.prototype,从而访问这些方法。
这背后是一个三层三角关系:
- 构造函数
MyQueue有prototype属性,指向原型对象 - 原型对象有
constructor属性,指回构造函数 - 实例有
__proto__属性,指向原型对象
ES6 的 class 只是这个三层关系的语法糖------typeof MyQueue 不管用哪种写法都是 "function"。
四、new 操作符的四步流程
new MyQueue() 的执行过程不是魔法,是四步操作:
- 创建一个空对象
{} - 将这个对象的
__proto__指向构造函数的prototype - 执行构造函数,
this指向新对象,添加属性 - 返回这个对象(除非构造函数显式返回了一个对象)
javascript
function myNew(constructor, ...args) {
const obj = {};
Object.setPrototypeOf(obj, constructor.prototype);
const result = constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
理解这四步之后,const queue = new MyQueue() 不再是一个黑盒。
五、总结
一道算法题加一个语言特性,二者的交汇点不是巧合:
- 用栈模拟队列考察的是数据结构组合思维
- prototype 机制是 JavaScript 面向对象的基石
- new 操作符 = 创建对象 → 绑定原型 → 初始化 → 返回
当你能从算法实现中看到语言底层的运转时,这两方面的知识就不再是孤立的两本书,而是同一块知识地图的不同区域。