撕开 JS 的 Class 面具:从构造函数的 new 降生到顶层原型链的终极通关
- 前言:没有类的面向对象?打破你的传统世界观
- 第一章:函数的双面人生——“一等公民”的秘密武器
-
- 幕后黑盒:函数本质上就是对象
- 第二章:视图与模板的内存大分工——`index.html` 的技术伏笔
-
- 架构师思维:为什么属性写体内,方法挂体外?
- 第三章:控制台的物理考古——揭秘“原型链三角铁律”与终极拉网
-
- 3.1 现场一:撕开实例的口袋
- 3.2 现场二:身份与关系的硬核等价
- 第四章:终极通关——`tx.toString()` 的跳跃链路
-
- 4.1 虚无的终点:null
- 4.2 殊途同归的字面量对象(结合图开源代码四)
- 🏁 总结:你学到的不是语法糖,是 JS 的世界观
前言:没有类的面向对象?打破你的传统世界观
对于许多从 C++、Java 或老版 C 语言转过来的开发者而言,“面向对象”往往与 class(类)这个词死死绑定在一起:类是写在纸上的标准图纸,实例是通过图纸实例化出来的物理房子。
然而,在 JavaScript 的世界里,存在着一个极具颠覆性的底层哲学:一切皆对象,唯独早期没有类(Class-less)。
在 ES6 引入 class 语法糖之前,JS 靠一套精妙绝伦的 “原型式面向对象(Prototypal OOP)” 玩转了世界。今天,我们将通过一行惊艳的 tx.toString(),配合浏览器控制台的“像素级物理考古”,彻底揭开 JavaScript 内存世界的底层潜规则。
第一章:函数的双面人生——“一等公民”的秘密武器
在深挖面向对象之前,我们必须先理解 JS 里的“函数”到底是个什么怪物。请看下面这段看似不可思议的代码:
function greeting(){
console.log('hello world');
}
greeting.a = '1'; // 👈 惊悚操作:给函数点(.)一个属性!
console.log(greeting.a); // 打印 '1'
greeting(); // 打印 'hello world'
幕后黑盒:函数本质上就是对象
在传统语言中,函数是一段写死的机器指令代码块。但在 JavaScript 里,函数是一等公民,且其本质就是一个超强对象。 当你声明一个函数时,引擎在堆内存(Heap)里开辟的空间其实是一个特殊的函数对象字典。它具有双重身份:
正是因为“函数本质上就是对象,能自由生长属性与方法”这一底层特权,才让它有资格化身为面向对象的终极发动机——构造函数(Constructor)。
第二章:视图与模板的内存大分工——index.html 的技术伏笔
在实际工程中,我们会约定首字母大写的函数来充当“类”的模版。让我们来看这篇博客的核心主线页面 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function Person(name, age){
// 构造实例的,实例私有的空间
console.log(this);
this.name = name;
this.age = age;
}
// 原型对象上的方法和属性是公用的(共享资产)
Person.prototype.poem = '仁义礼智信';
Person.prototype.say = function(){
console.log(`我叫${this.name},很高兴认识你`);
}
Person.prototype.timeMF = function(){
console.log('时间管理魔法');
}
const tx = new Person('苔藓', 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 现场二:身份与关系的硬核等价

在上图中,控制台敲下了两行灵魂拷问,得到了大写的 true:
> tx.__proto__ === Person.prototype
< true
> tx.__proto__.constructor
< ƒ Person(name, age) { … }
** 这直接印证了 new 运算符在幕后黑盒里偷干的“四部曲”**:
同时,原型对象 Person.prototype 身上自带一个 constructor 属性,它像回音壁一样,精准反向指回了构造函数 Person 本身。“实例、构造函数、原型对象”三者自此结成了坚不可摧的铁三角关系。
第四章:终极通关——tx.toString() 的跳跃链路
现在,我们可以完美回答那个悬念了:为什么 tx.toString() 能够成功运行?
当你在代码最后一行执行 tx.toString() 时,引擎并不会魔法,它是一个顺着 __proto__ 链条疯狂爬行的“剥洋葱”捕快:

在图中,输入:
> tx.__proto__.__proto__ === Object.prototype
< true
铁证凿实! 在 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__ 也毫无例外地直通大boss Object.prototype。任何对象,在抵达终点 null 前的一站,必定是 Object.prototype!
🏁 总结:你学到的不是语法糖,是 JS 的世界观
通过这节课的深入探秘,我们彻底打破了对 class 的迷信。
JavaScript 的面向对象从来不是靠僵硬的图纸去复刻,而是靠对象与对象之间通过 __proto__ 织就的一条血脉相连的“传送带”。
- 实例靠 __proto__ 向上孝敬原型。
- 原型靠 constructor 认祖归宗指回构造函数。
- 萬物沿着链条向上攀爬,汇流于 Object.prototype,最终归于 null 的虚无。
吃透了控制台打印出来的 true 与 null,你就彻底拿捏了 JavaScript 最硬核、最底层的灵魂。





