🚗 想要一辆小米 SU7?先搞懂 JS 的“原型工厂”!
导读:在 ES6 之前,JavaScript 没有
class,那它是怎么实现“面向对象”的?难道全靠“拼凑”? 别被术语吓跑,今天我们把枯燥的 原型(Prototype)、构造函数 和 原型链 比喻成一家**“超级汽车制造厂”**。看完这篇,你不仅能听懂技术黑话,还能彻底看懂那张让无数人头秃的原型机制图!
🏭 第一章:没有“类”,我们怎么造车?
在传统语言(如 Java)里,想造一辆车,你得先画图纸(定义 Class),再按图纸生产(new Instance)。 但在 ES5 时代的 JavaScript 里,没有图纸(Class),只有**“模具函数”(Constructor Function)**。
1. 构造函数:热情的流水线工人
想象 Car 不是一个图纸,而是一个拿着喷枪的工人。当你喊 new Car() 时,他立马动手给你捏一辆车出来。
// 这是一个构造函数(首字母大写是约定俗成的规矩)
function Car(color) {
// this 指向那个刚刚被捏出来的、崭新的空对象
this.color = color;
this.name = 'SU7';
this.height = 1.5;
this.length = 4800;
// ⚠️ 注意:如果在这里写方法,每造一辆车,就要复制一份方法代码,太浪费内存了!
// this.drive = function() { ... } <-- 千万别这么干!
}
// 开始造车!
const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');
console.log(car1); // 输出:Car { color: '霞光紫', name: 'SU7', ... }
关键点:
new关键字是魔法咒语,它让this指向新创建的对象。- 每个实例(
car1,car2)都有自己独立的属性(颜色不同),这叫**“私有财产”**。
📦 第二章:prototype —— 公共配件仓库
如果每辆车都把“发动机启动方法”、“导航系统”复制一份存在自己身上,那内存早就爆了!
于是,JS 发明了 prototype(原型对象)。
比喻:
prototype 就是工厂旁边的**“公共配件仓库”**。
- 所有的车(实例)都不自带这些大件方法。
- 当车需要“启动”或“漂移”时,它会跑去仓库里借来用一下。
- 好处:仓库里只放一份代码,一万辆车共享,省空间!
// 给 Car 的公共仓库添加方法
Car.prototype = {
drive() {
console.log(`🏎️ ${this.color} 的 SU7 正在下赛道漂移!`);
},
honk() {
console.log("滴滴!");
}
};
// 现在试试调用
car1.drive(); // 输出:🏎️ 霞光紫 的 SU7 正在下赛道漂移!
car2.drive(); // 输出:🏎️ 海湾蓝 的 SU7 正在下赛道漂移!
// 验证:它们用的是同一个方法吗?
console.log(car1.drive === car2.drive); // true ✅ 省内存实锤!
🔗 第三章:揭秘那张“让人头秃”的关系图
你提供的 图片里那些箭头到底是什么意思?让我们把它们翻译成“人话”。
🧩 核心三角关系
-
构造函数 (
Person/Car) ➡️prototype属性 ➡️ 原型对象- 含义:工厂手里拿着仓库的钥匙。
- 代码:
Car.prototype指向那个公共仓库对象。
-
实例对象 (
car1) ➡️__proto__属性 ➡️ 原型对象- 含义:每辆造出来的车,后备箱里都藏着一根绳子(
__proto__),绳子的另一头死死绑在公共仓库上。 - 代码:
car1.__proto__ === Car.prototype(true)。 - 注意:这是 JS 引擎自动连上的,你不用手动操作。
- 含义:每辆造出来的车,后备箱里都藏着一根绳子(
-
原型对象 ➡️
constructor属性 ➡️ 构造函数- 含义:仓库墙上挂着一块牌子,写着“本仓库归属于 XX 工厂”。
- 代码:
Car.prototype.constructor === Car。 - 作用:让实例知道自己是哪个工厂造的(
car1.constructor)。
🌲 原型链:找不到就往上问爷爷
如果你在车上找一个属性(比如 toString),车上没有怎么办?
- 顺着
__proto__绳子去 公共仓库 (Car.prototype) 找。 - 如果仓库也没有?别急,仓库本身也是个对象,它也有
__proto__! - 它会指向 万能总仓库 (
Object.prototype)。这里放着所有对象共用的方法(如toString,valueOf)。 - 如果总仓库还没有?再往上找... 哦,
Object.prototype.__proto__是null。 - 终点:
null表示“没爹了”,停止查找,返回undefined。
这就是原型链(Prototype Chain)!
💣 第四章:避坑指南与代码实战
看几个你提供的代码片段中的“陷阱”和“真相”。
陷阱 1:中途换仓库,旧车会迷路吗?
function Person(name, age) {
this.name = name;
}
var p1 = new Person('舒总', 15);
// ❌ 危险操作:直接替换了整个 prototype 对象
Person.prototype = {
speci: '人类'
};
// 此时 p1 的 __proto__ 依然指向【旧的】prototype 对象
// 而新的 Person.prototype 是【新的】对象
console.log(p1.__proto__ === Person.prototype); // false ❌
console.log(p1.speci); // undefined ❌ 旧车找不到新仓库的属性!
// ✅ 正确做法:不要直接赋值 {},而是往现有的 prototype 里加属性
// Person.prototype.speci = '人类';
结论:new 的那一刻,连线就确定了。后来你换了仓库地址,旧车是不会自动更新的!
陷阱 2:实例修改属性,会污染仓库吗?
function Person() {}
Person.prototype.species = '人类';
var su = new Person();
// 试图修改
su.species = 'LOL 达人';
console.log(su.species); // 输出:'LOL 达人'
console.log(su.__proto__.species); // 输出:'人类' ✅ 仓库没被污染!
// 原理:
// 当执行 su.species = '...' 时,JS 发现 su 自己身上没有 species,
// 于是直接在 su 自己身上创建了一个新的 species 属性。
// 这叫做 **“属性遮蔽” (Shadowing)**。
// 下次再找 species,先在 su 身上找到了,就不会再去原型链上了。
进阶:ES6 Class 只是“糖衣炮弹”
你提供的代码里还有 ES6 的写法:
class Person {
constructor(name) { this.name = name; }
sayHi() { console.log(`Hi ${this.name}`); }
}
真相:
class 语法出现后,很多人以为 JS 变成基于“类”的语言了。
错! 它依然是基于原型的!
class只是让写法更像 Java/C#。- 底层编译后,
sayHi依然被放在了Person.prototype上。 su.__proto__ === Person.prototype依然为true。
🎯 总结:一张图带走核心逻辑
回到你提供的那张神图,现在的你应该能秒懂了:
- Person (构造函数):通过
prototype属性,指着 Person.prototype (原型对象)。 - person (实例):通过
__proto__(蓝色箭头),指着 Person.prototype。 - Person.prototype:通过
constructor,指回 Person。 - 继承链条:Person.prototype 通过
__proto__指着 Object.prototype,最后指向 null。
💡 终极口诀
构造函数建工厂,prototype 是仓库房。 实例对象 newborn,proto 连仓库墙。 属性查找顺链走,直到 null 才收场。 constructor 认爹娘,class 语法是伪装!
JavaScript 的这种“原型式面向对象”,虽然不像传统类那样有严格的血缘关系,但它极其灵活。理解了它,你就拿到了通往 JS 高阶开发的钥匙!🔑