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

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

前言:没有类的面向对象?打破你的传统世界观

对于许多从 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)里开辟的空间其实是一个特殊的函数对象字典。它具有双重身份:

  1. 普通对象的一面: 它和普通对象 {} 一样,可以任意挂载键值对。执行 greeting.a = '1',就是往这个对象的属性空间里塞入了一个属性 a
  2. 可调用的另一面(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 运算符在幕后黑盒里偷干的"四部曲"**:

  1. 造空房: let obj = {}; 在内存里开辟全新空对象。
  2. 织纽带: obj.__proto__ = Person.prototype;(图中来源)。
  3. 魂附体:Person 内部的 this 强行指向这个 obj,开始执行 this.name = '苔藓',让空房长出私有属性。
  4. 交钥匙: 自动返回这个打通了任督二脉的实例,赋值给变量 tx

同时,原型对象 Person.prototype 身上自带一个 constructor 属性,它像回音壁一样,精准反向指回了构造函数 Person 本身。"实例、构造函数、原型对象"三者自此结成了坚不可摧的铁三角关系


第四章:终极通关------tx.toString() 的跳跃链路

现在,我们可以完美回答那个悬念了:为什么 tx.toString() 能够成功运行?

当你在代码最后一行执行 tx.toString() 时,引擎并不会魔法,它是一个顺着 __proto__ 链条疯狂爬行的"剥洋葱"捕快:

  1. 第一站(tx 自身): 引擎翻了翻 tx 自身的口袋,发现里面只有 {name: '苔藓', age: 18}。没有 toString
  2. 第二站(Person.prototype): 引擎顺着 tx.__proto__ 瞬间位移到 Person.prototype。在这里翻出了 poemsaytimeMF。依然没有 toString
  3. 第三站(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 的虚无。

吃透了控制台打印出来的 truenull,你就彻底拿捏了 JavaScript 最硬核、最底层的灵魂。

相关推荐
拾年2754 小时前
从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
前端·javascript·ajax
拉勾科研工作室4 小时前
区块链工程毕业论文题目【249个】
开发语言·javascript
小林ixn4 小时前
你以为你懂 + 号?看完这篇 Bun + TS 实战,才发现以前全写错了
前端·javascript·typescript
z落落5 小时前
C#WinForm控件实战:Panel与单选框动态创建
开发语言·c#
ptc学习者5 小时前
python 中描述符@property property 大概的样子
开发语言·python
zmzb01035 小时前
Python课后习题训练记录Day129
开发语言·python
张忠琳5 小时前
【Go 1.26.4】Golang Map 深度解析
开发语言·后端·golang
Vertira5 小时前
如何对QT开发的软件进行打包[已解决]
开发语言·qt
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第110题】【并发篇】第10题:CAS 存在哪些问题?
java·开发语言·面试
石一峰6995 小时前
C 语言函数设计模式实战经验
c语言·开发语言·设计模式