从两摞盘子到 JS 原型链——一场蓄谋已久的“降维打击“
从两摞盘子到 JS 原型链——一场蓄谋已久的降维打击缘起队列不过是个调度员诸位请先看一道经典题目用栈模拟队列。没学过数据结构的同学此刻可能在想——栈是什么队列又是什么能吃吗容我用生活场景打个比方。栈就是一摞盘子。你只能从最上面拿也只能往最上面放这种悲催的后进先出简称FILO——First In, Last Out。队列就是食堂打饭的队伍。先来的先打、后来的后打这就是文明的先进先出简称FIFO——First In, First Out。题目要求用两摞盘子模拟一个打饭队伍。乍一听这就像让你用两把叉子喝汤——工具不对啊但你可以这样来一个人我们把他放进第一摞盘子里stack1.push。要出队的时候呢把第一摞盘子里的所有人依次倒到第二摞盘子里——这时候原本在最底下的那个最早来的人就跑到了第二摞的最上面pop一下就出去了。然后再把剩下的人倒回去。妙啊这招叫乾坤大挪移。两个栈一个负责进一个负责出——本质上是用空间换时间用两次翻转来化解 FILO 和 FIFO之间的根本矛盾。你以为你在学算法题其实你在学哲学有些看似不可调和的矛盾加一层中间层就解决了。队列是栈的抽象抽象是计算机科学的灵魂。暴论JavaScript 根本没有类当你满怀信心地打开 1.js准备写一个 MyQueue 类的时候写着写着发现不对劲——怎么没有 class 关键字那个时代ES6 之前JS 的 class 还在娘胎里。但人家照样做面向对象开发靠的是什么函数 prototype。const MyQueue function () {this.stack1 [];this.stack2 [];}MyQueue.prototype.push function() { … }我第一次看到这段代码的时候内心是崩溃的。函数就函数怎么还能 .prototype这到底是函数还是对象答案是——都是。JavaScript 的设计者 Brendan Eich 在 1995 年花了十天写出了这门语言。十天的产品你不能要求它像 Java那样西装革履。但它有一个极为大胆、极为天才的内核设计一切皆是对象原型链替代类继承。这就引出第二个暴论函数也是对象而且是一等公民。看一下 2.jsfunction greeting() { console.log(‘hello world’); }greeting.a ‘q’;console.log(greeting.a); // ‘q’你给一个函数动态添加了属性就跟给普通对象加属性一样自然。这在 Java开发者眼里属于大逆不道——函数怎么可以有属性但 JS 说凭什么不可以函数不仅是可执行的代码块它还是一个可以携带数据的容器。这种身兼两职的设计后来成为了前端生态的基石——回调函数、高阶函数、闭包、装饰器……全是这个思想开出的花。一条链子串起万千对象4.html 和 readme.md 里详细记录了 new 关键字和原型链的完整知识我用大白话重新串一遍。当你写 const zwy new Person(‘zwy’, 18) 的时候JS 悄悄干了四件事创建一个空对象 {}让这个空对象的proto指向 Person.prototype把构造函数里的 this 指向这个空对象执行构造函数给 this 塞属性返回这个对象除非你手动 return 了别的东西其中第二步就是 JS 面向对象体系最精妙的一环——原型链。来看看 readme.md 里亲手在 Console 做的实验zwy.proto Person.prototype // truezwy.proto.proto Object.prototype // truezwy.proto.proto.proto// null一条链串了三层zwy 自身存了你造的独有属性name、agePerson.prototype存了共享方法say、timeMF和共享属性poemObject.prototype存了万物之祖的方法toString、hasOwnProperty 等null链的终点一切归于虚无当你调用 zwy.say() 时JS 先在自己身上找 say。找不到那就沿着proto去 Person.prototype上找。还找不到继续沿着proto.proto去 Object.prototype 找。一直找到 null找不到就报 undefined。这不就是一个天然的责任链模式吗设计模式书里要写十几页的东西JS 从娘胎里就带着。Brendan Eich这十天的工作效率我哭死。议原型继承是 Bug 还是 Feature很多从 Java/C 转过来的程序员痛斥 JS 的原型链是反人类的设计——没有私有属性没有真正的类继承要靠手动连prototypethis 指向说变就变……这些批评都有道理。但我想换个角度原型继承是一种极简版的面向对象。类的本质是什么是模板和实例的关系。原型直接跳过了模板这个概念——你不需要先定义一个抽象的类你直接拿一个已经存在的对象当原型然后在这个基础上造出相似但不同的新对象。这像什么像生物进化。你不是从人类图纸上生产出来的你是从一个具体的人你爸妈的基因组合变异出来的。原型链就是物种的进化树——从 Object.prototype 这个单细胞生物一路分化出 Array、Function、Date……从这个角度看原型继承比类继承更贴近这个世界的运行方式。现实中本来就没有抽象的完美模板——只有不断复制、变异、适应的具体个体。当然ES6 后来还是加了 class 关键字。但你心里要清楚——那只是语法糖底下跑的还是原型链。JS的设计哲学从未改变它只是换了一张更友好的皮。教学时刻三个灵魂拷问学到这里你需要能回答三个问题第一问prototype 和proto到底什么关系prototype 是函数的属性——“我造出来的实例共享方法上这儿找”。proto是实例的属性——“我自己的原型对象是谁”两者指向同一个对象但站在不同的立场上。第二问为什么把方法放在 prototype 上而不是构造函数里构造函数里定义的方法每个实例都会拷贝一份——一百个实例就是一百份一模一样的函数浪费内存。放在 prototype上一百个实例共享同一份——省内存而且运行时还能热更新改一次原型所有实例立刻生效。第三问栈模拟队列到底怎么写完整代码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) {this.stack2.push(this.stack1.pop());}}return this.stack2.pop();}MyQueue.prototype.peek function() {if (this.stack2.length 0) {while (this.stack1.length) {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;}用原型式面向对象写出来的队列行云流水毫不违和。结语这个小小的 stack_queue 目录表面上在讲一道算法题和一个 JS 知识点实际上它是两种思维的碰撞数据结构思维栈的 FILO 和队列的 FIFO 是矛盾加一层栈做中转矛盾化解——这是工程的智慧。语言设计思维类继承和原型继承是两种世界观JS选了更野的那条路然后用二十年的时间证明——野路子也能长出参天大树。下次有人嘲笑你JS 连类都没有你可以微笑着回一句▎ “是的但它有原型链。而你的类本质上也不过是原型链上的一颗语法糖罢了。”