撕开 JS 的 Class 面具:从构造函数的 new 降生到顶层原型链的终极通关
- 前言:没有类的面向对象?打破你的传统世界观
- 第一章:函数的双面人生------"一等公民"的秘密武器
- [第二章:视图与模板的内存大分工------`index.html` 的技术伏笔](#第二章:视图与模板的内存大分工——
index.html的技术伏笔) - 第三章:控制台的物理考古------揭秘"原型链三角铁律"与终极拉网
-
- [3.1 现场一:撕开实例的口袋](#3.1 现场一:撕开实例的口袋)
- [3.2 现场二:身份与关系的硬核等价](#3.2 现场二:身份与关系的硬核等价)
- [第四章:终极通关------`tx.toString()` 的跳跃链路](#第四章:终极通关——
tx.toString()的跳跃链路) -
- [4.1 虚无的终点:null](#4.1 虚无的终点:null)
- [4.2 殊途同归的字面量对象(结合图开源代码四)](#4.2 殊途同归的字面量对象(结合图开源代码四))
- [🏁 总结:你学到的不是语法糖,是 JS 的世界观](#🏁 总结:你学到的不是语法糖,是 JS 的世界观)
前言:没有类的面向对象?打破你的传统世界观
对于许多从 C++、Java 或老版 C 语言转过来的开发者而言,"面向对象"往往与 class(类)这个词死死绑定在一起:类是写在纸上的标准图纸,实例是通过图纸实例化出来的物理房子。
然而,在 JavaScript 的世界里,存在着一个极具颠覆性的底层哲学:一切皆对象,唯独早期没有类(Class-less)。
在 ES6 引入 class 语法糖之前,JS 靠一套精妙绝伦的 "原型式面向对象(Prototypal OOP)" 玩转了世界。今天,我们将通过一行惊艳的 tx.toString(),配合浏览器控制台的"像素级物理考古",彻底揭开 JavaScript 内存世界的底层潜规则。
第一章:函数的双面人生------"一等公民"的秘密武器
在深挖面向对象之前,我们必须先理解 JS 里的"函数"到底是个什么怪物。请看下面这段看似不可思议的代码:
javascript
function greeting(){
console.log('hello world');
}
greeting.a = '1'; // 👈 惊悚操作:给函数点(.)一个属性!
console.log(greeting.a); // 打印 '1'
greeting(); // 打印 'hello world'
幕后黑盒:函数本质上就是对象
在传统语言中,函数是一段写死的机器指令代码块。但在 JavaScript 里,函数是一等公民,且其本质就是一个超强对象 。
当你声明一个函数时,引擎在堆内存(Heap)里开辟的空间其实是一个特殊的函数对象字典。它具有双重身份:
- 普通对象的一面: 它和普通对象
{}一样,可以任意挂载键值对。执行greeting.a = '1',就是往这个对象的属性空间里塞入了一个属性a。 - 可调用的另一面(Callable): 这个特殊对象在底层被赋予了一个隐藏的内部属性
[[Call]],里面存放着你写的可执行代码块。当你加上括号greeting()时,引擎就会触发这个机关去执行里面的代码。
正是因为"函数本质上就是对象,能自由生长属性与方法"这一底层特权,才让它有资格化身为面向对象的终极发动机------构造函数(Constructor)。
第二章:视图与模板的内存大分工------index.html 的技术伏笔
在实际工程中,我们会约定首字母大写 的函数来充当"类"的模版。让我们来看这篇博客的核心主线页面 index.html:
javascript
<!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 并回车,控制台打印出了:
javascript
Person {name: '苔藓', age: 18}
这证明了构造函数通过 this 确实把私有属性扣在了 tx 自身身上。
然而当你展开 tx.__proto__(即隐式原型属性)时,魔术发生了
!
控制台赫然吐出了:{poem: '仁义礼智信', say: ƒ, timeMF: ƒ, constructor: ƒ}。
🔍 考古发现:
任何通过
new传授降生的实例对象,身上都自带一个隐藏的秘密通道__proto__。它死死地指向了其父亲的公共资产库------Person.prototype。
3.2 现场二:身份与关系的硬核等价

在上图中,控制台敲下了两行灵魂拷问,得到了大写的 true:
javascript
> tx.__proto__ === Person.prototype
< true
> tx.__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。
在图中,输入:
javascript
> tx.__proto__.__proto__ === Object.prototype
< true
铁证凿实! 在 Object.prototype 的皇家公共库里,静静地躺着内建的 toString() 方法。引擎一霸抓取,回传执行,全网通关!
4.1 虚无的终点:null
如果到了世界之巅 Object.prototype 还找不到方法呢?在上图三的第二行,探测了整条铁链的物理物理边界:
javascript
> 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 最硬核、最底层的灵魂。