🚗 一、引子:如何“拿下”一辆小米 SU7?
想象你要拥有一辆 小米 SU7。在 JavaScript 的世界里,这辆车不是凭空出现的,而是通过以下方式“制造”出来的:
- 构造函数
Car→ 工厂的“生产规则” new Car()→ 你手中的那辆具体 SU7(实例)Car.prototype→ 所有 SU7 共享的“功能库”(自动泊车、赛道模式等)
function Car(color) {
this.color = color; // 实例独有属性:你的车是霞光紫 or 海湾蓝? }
// 所有 SU7 共享的功能
Car.prototype = { // Car.prototype 被重新赋值为一个新对象
name: '小米SU7',
drive() {
console.log('drive, 下赛道!');
}
};
const car1 = new Car('霞光紫');
car1.drive(); // 沿着原型链向上查找到,"drive, 下赛道!"
这就是 JavaScript 原型式面向对象的起点。
🔑 二、核心思想:JS 没有“类”,只有对象委托
很多人误以为 JS 是“类继承”,其实大错特错!
✅ 传统 OOP(如 Java):
- 血缘关系:先定义抽象的“类模板”,再实例化
- 类是蓝图,实例是产品
✅ JavaScript 原型式 OOP:
- 委托关系:对象直接继承对象
- 没有“类”,只有构造函数 + 原型对象
- 实例找不到的方法,会通过
__proto__委托给原型查找
💡 哲学差异:
“世界由具体对象构成,新对象可克隆并扩展已有对象” —— 而非先定义抽象类别。
(注:“孔子是韩国的”是个玩笑,提醒我们别被传统思维绑架 😄)
🧩 三、两大支柱:构造函数 vs 原型
| 角色 | 职责 | 存放内容 | 内存 |
|---|---|---|---|
| 构造函数 | 初始化实例 | 每个实例独有的数据 (如 color, owner) | 每个实例一份 |
原型(prototype) | 提供共享行为 | 所有实例共用的方法/常量 (如 drive(), brand = '小米') | 所有实例共享一份 |
function Person(name, age) {
this.name = name; // 独有
this.age = age; // 独有
}
Person.prototype.speci = '人类'; // 共享
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
const p1 = new Person('小王', 19);
p1.sayHi(); // "你好,我是小王"
🔗 四、原型链:从实例到宇宙尽头
当你调用 p1.toString() 时,JS 引擎做了什么?
原型链查找过程:
- 在
p1自身找toString→ ❌ - 去
p1.__proto__(即Person.prototype)找 → ❌ - 去
Person.prototype.__proto__(即Object.prototype)找 → ✅ - 执行
Object.prototype.toString
完整原型链:
✅ 验证:
console.log(p1.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
⚖️ 五、优先级规则:实例属性 vs 原型属性
- 实例属性优先级更高(属性遮蔽)
- 实例有同名属性 → 直接使用,不会去原型找
- 删除实例属性后 → 自动回退到原型
Person.prototype.species = '人类';
const p1 = new Person('张三', 18);
p1.species = '动物'; // 实例属性遮蔽
console.log(p1.species); // '动物'
delete p1.species;
console.log(p1.species); // '人类' ← 回退到原型
⚠️ 六、常见陷阱与最佳实践
1. 重写 prototype 会丢失 constructor
Person.prototype = { sayHi() {} };
// 此时:Person.prototype.constructor === Object ❌
// ✅ 修复:
Person.prototype = {
constructor: Person, // 👈 必须手动补上
sayHi() {}
};
2. 修改原型会影响所有实例(包括已创建的!)
Person.prototype.wheels = 4;
console.log(p1.wheels); // 4 ← 即使 p1 创建于修改之前!
3. 推荐使用标准 API
- 获取原型:
Object.getPrototypeOf(obj) - 设置原型:
Object.setPrototypeOf(obj, proto)(慎用) -带你彻底搞懂 JavaScript 的原型机制 判断自有属性:obj.hasOwnProperty('name')
🔄 七、ES6 class 只是语法糖
// ES5
function Car(brand) {
this.brand = brand;
}
Car.prototype.drive = function() { /*...*/ };
// ES6
class Car {
constructor(brand) {
this.brand = brand;
}
drive() { /*...*/ }
}
💡 底层完全一致!class 中的方法依然定义在 Car.prototype 上。
🧪 八、实战:模拟“继承”(原型链延伸)
function Vehicle() {}
Vehicle.prototype.move = function() {
console.log('移动中...');
};
function Car(color) {
this.color = color;
}
Car.prototype = new Vehicle(); // 继承 Vehicle
Car.prototype.constructor = Car;
const su7 = new Car('珍珠白');
su7.move(); // "移动中..." ← 来自父类原型
这就是早期 ES5 实现“继承”的方式:让子类原型指向父类实例。
📌 九、总结:一句话掌握原型
JavaScript 不靠“类”继承,而靠“对象委托”;实例通过 __proto__ 链接到原型,共享行为 —— 这就是原型式面向对象的灵魂
🔚 结语
从“拿下小米 SU7”出发,我们走过了:
- 构造函数与原型的职责划分
__proto__与prototype的区别- 原型链的查找机制
- 属性遮蔽与共享
- ES5 与 ES6 的本质统一
现在,你不仅能“开” SU7,还能“造” SU7,甚至“改装” SU7!
🌟 真正的高手,不仅会用轮子,更懂得轮子是怎么转的
欢迎点赞、收藏、评论!
如果你觉得这篇文章帮你理清了原型的迷雾,不妨分享给更多正在“造车”的朋友 🚗💨
作者:想要成为糕糕手