撕开 JS 的 Class 面具:从构造函数的 new 降生到顶层原型链的终极通关

撕开 JS 的 Class 面具:从构造函数的 new 降生到顶层原型链的终极通关 撕开 JS 的 Class 面具从构造函数的 new 降生到顶层原型链的终极通关前言没有类的面向对象打破你的传统世界观第一章函数的双面人生——“一等公民”的秘密武器幕后黑盒函数本质上就是对象第二章视图与模板的内存大分工——index.html 的技术伏笔架构师思维为什么属性写体内方法挂体外第三章控制台的物理考古——揭秘“原型链三角铁律”与终极拉网3.1 现场一撕开实例的口袋3.2 现场二身份与关系的硬核等价第四章终极通关——tx.toString() 的跳跃链路4.1 虚无的终点null4.2 殊途同归的字面量对象结合图开源代码四 总结你学到的不是语法糖是 JS 的世界观前言没有类的面向对象打破你的传统世界观对于许多从 C、Java 或老版 C 语言转过来的开发者而言“面向对象”往往与class类这个词死死绑定在一起类是写在纸上的标准图纸实例是通过图纸实例化出来的物理房子。然而在 JavaScript 的世界里存在着一个极具颠覆性的底层哲学一切皆对象唯独早期没有类Class-less。在 ES6 引入class语法糖之前JS 靠一套精妙绝伦的“原型式面向对象Prototypal OOP”玩转了世界。今天我们将通过一行惊艳的tx.toString()配合浏览器控制台的“像素级物理考古”彻底揭开 JavaScript 内存世界的底层潜规则。第一章函数的双面人生——“一等公民”的秘密武器在深挖面向对象之前我们必须先理解 JS 里的“函数”到底是个什么怪物。请看下面这段看似不可思议的代码functiongreeting(){console.log(hello world);}greeting.a1;// 惊悚操作给函数点.一个属性console.log(greeting.a);// 打印 1greeting();// 打印 hello world幕后黑盒函数本质上就是对象在传统语言中函数是一段写死的机器指令代码块。但在 JavaScript 里函数是一等公民且其本质就是一个超强对象。当你声明一个函数时引擎在堆内存Heap里开辟的空间其实是一个特殊的函数对象字典。它具有双重身份普通对象的一面它和普通对象{}一样可以任意挂载键值对。执行greeting.a 1就是往这个对象的属性空间里塞入了一个属性a。可调用的另一面Callable这个特殊对象在底层被赋予了一个隐藏的内部属性[[Call]]里面存放着你写的可执行代码块。当你加上括号greeting()时引擎就会触发这个机关去执行里面的代码。正是因为“函数本质上就是对象能自由生长属性与方法”这一底层特权才让它有资格化身为面向对象的终极发动机——构造函数Constructor。第二章视图与模板的内存大分工——index.html的技术伏笔在实际工程中我们会约定首字母大写的函数来充当“类”的模版。让我们来看这篇博客的核心主线页面index.html!DOCTYPEhtmlhtml langenheadmeta charsetUTF-8meta nameviewportcontentwidthdevice-width, initial-scale1.0titleDocument/title/headbodyscriptfunctionPerson(name,age){// 构造实例的实例私有的空间console.log(this);this.namename;this.ageage;}// 原型对象上的方法和属性是公用的共享资产Person.prototype.poem仁义礼智信;Person.prototype.sayfunction(){console.log(我叫${this.name},很高兴认识你);}Person.prototype.timeMFfunction(){console.log(时间管理魔法);}consttxnewPerson(苔藓,18);console.log(tx.toString());// 终极悬念Person 里明明没写 toString为何不报错/script/body/html架构师思维为什么属性写体内方法挂体外在这段代码中你展现了极其漂亮的内存性能优化技巧体内this.name name属性是每个实例私有的苔藓是 18 岁别的人可能是 20 岁。通过this把属性直接绑在每个新诞生对象的独立内存里互不干扰。体外Person.prototype.say方法是公用的。所有实例的说话逻辑一模一样。如果在体内写this.say function...当new出一万个对象时内存里就会疯狂复制一万个一模一样的函数内存瞬间死机。挂在显式原型prototype上在内存中该方法永远只有一份所有实例共享访问高阶榨干内存性能。第三章控制台的物理考古——揭秘“原型链三角铁律”与终极拉网现在我们以四张控制台现场截图登场来讲解。它们用无视辩驳的运行时铁证彻底还原了const tx new Person(苔藓, 18);执行后整个堆内存织就的三角拓扑网。3.1 现场一撕开实例的口袋当你输入tx并回车控制台打印出了Person{name:苔藓,age:18}这证明了构造函数通过this确实把私有属性扣在了tx自身身上。然而当你展开tx.__proto__即隐式原型属性时魔术发生了!控制台赫然吐出了{poem: 仁义礼智信, say: ƒ, timeMF: ƒ, constructor: ƒ}。 考古发现任何通过new传授降生的实例对象身上都自带一个隐藏的秘密通道__proto__。它死死地指向了其父亲的公共资产库——Person.prototype。3.2 现场二身份与关系的硬核等价在上图中控制台敲下了两行灵魂拷问得到了大写的truetx.__proto__Person.prototypetruetx.__proto__.constructorƒPerson(name,age){...}** 这直接印证了new运算符在幕后黑盒里偷干的“四部曲”**造空房let obj {};在内存里开辟全新空对象。织纽带obj.__proto__ Person.prototype;图中来源。魂附体让Person内部的this强行指向这个obj开始执行this.name 苔藓让空房长出私有属性。交钥匙自动返回这个打通了任督二脉的实例赋值给变量tx。同时原型对象Person.prototype身上自带一个constructor属性它像回音壁一样精准反向指回了构造函数Person本身。“实例、构造函数、原型对象”三者自此结成了坚不可摧的铁三角关系。第四章终极通关——tx.toString()的跳跃链路现在我们可以完美回答那个悬念了为什么tx.toString()能够成功运行当你在代码最后一行执行tx.toString()时引擎并不会魔法它是一个顺着__proto__链条疯狂爬行的“剥洋葱”捕快第一站tx 自身引擎翻了翻tx自身的口袋发现里面只有{name: 苔藓, age: 18}。没有toString。第二站Person.prototype引擎顺着tx.__proto__瞬间位移到Person.prototype。在这里翻出了poem、say和timeMF。依然没有toString第三站Object.prototype结合上图引擎绝不放弃。Person.prototype本身也是个对象它也有自己的__proto__。于是引擎沿着Person.prototype.__proto__发起了更高级别的空间跳跃瞬间抵达了全 JS 对象的共同始祖Object.prototype。在图中输入tx.__proto__.__proto__Object.prototypetrue铁证凿实在Object.prototype的皇家公共库里静静地躺着内建的toString()方法。引擎一霸抓取回传执行全网通关4.1 虚无的终点null如果到了世界之巅Object.prototype还找不到方法呢在上图三的第二行探测了整条铁链的物理物理边界tx.__proto__.__proto__.__proto__null当撞向null无/虚无时意味着整条链条彻底摸到了底。如果此时还没找到方法引擎就会当场崩溃对你抛出绝望的TypeError: is not a function。4.2 殊途同归的字面量对象结合图开源代码四有些同学可能会问我平时不写构造函数我直接写一句话var obj {name: 苔藓}这也有原型链吗在图四中你给出了教科书级别的答案代码片真相大白var obj {}在 JS 引擎眼里底层就是隐式执行了new Object()。所以即便是一个最普通的对象它的隐式原型__proto__也毫无例外地直通大bossObject.prototype。任何对象在抵达终点null前的一站必定是Object.prototype 总结你学到的不是语法糖是 JS 的世界观通过这节课的深入探秘我们彻底打破了对class的迷信。JavaScript 的面向对象从来不是靠僵硬的图纸去复刻而是靠对象与对象之间通过__proto__织就的一条血脉相连的“传送带”。实例靠__proto__向上孝敬原型。原型靠constructor认祖归宗指回构造函数。萬物沿着链条向上攀爬汇流于Object.prototype最终归于null的虚无。吃透了控制台打印出来的true与null你就彻底拿捏了 JavaScript 最硬核、最底层的灵魂。