一句话总结:在 JavaScript 中,“类”不是靠血缘继承,而是靠原型链“借”方法和属性。想开上“小米 SU7”?先学会用
prototype造一辆!
🚗 一、目标:如何“拿到”一辆小米 SU7?
假设我们要用代码模拟一辆小米 SU7,它有颜色、尺寸等属性,还能“下赛道”(drive() 方法)。
在传统面向对象语言(如 Java)中,我们会定义一个 Car 类;但在 ES5 的 JavaScript 中,我们用 构造函数 + 原型(prototype) 来实现。
✅ 正确姿势:构造函数 + prototype
javascript
编辑
// 构造函数:负责实例私有属性(如颜色)
function Car(color) {
this.color = color; // 每辆车颜色可以不同
}
// 原型对象:共享的方法和公共属性(所有车都一样)
Car.prototype = {
name: 'SU7',
height: 1.4,
weight: 1.5,
long: 4800,
drive() {
console.log('Drive! 下赛道~');
}
};
// 创建两辆不同颜色的 SU7
const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');
console.log(car1.name); // 'SU7'(来自原型)
console.log(car1.color); // '霞光紫'(自身属性)
car1.drive(); // "Drive! 下赛道~"
💡 关键点:
this.color是每个实例独有的;name、drive()等定义在prototype上,被所有实例共享,节省内存。
🔍 二、深入理解 prototype:JS 的“哲学”
JavaScript 的面向对象是 基于原型(Prototype-based) ,而非传统“类继承”。它的核心思想是:
“我不是生来就有这些能力,但我可以借用别人的。”
1. 每个函数都有 prototype 属性
- 构造函数(如
Car)是一个函数,因此它有Car.prototype。 - 这个
prototype是一个普通对象,默认包含一个constructor属性指向构造函数本身。
javascript
编辑
console.log(Car.prototype.constructor === Car); // true(未重写时)
2. 每个实例都有 __proto__(或 [[Prototype]])
- 当你
new Car()时,新对象的__proto__会自动指向Car.prototype。 - 这就是原型链查找机制的起点。
javascript
编辑
console.log(car1.__proto__ === Car.prototype); // true
3. 原型链终点:Object.prototype
- 所有原型最终都会链到
Object.prototype。 Object.prototype.__proto__ === null,查找在此终止。
javascript
编辑
console.log(car1.toString()); // 能调用!因为 toString 在 Object.prototype 上
console.log(car1.__proto__.__proto__ === Object.prototype); // true
⚠️ 三、常见陷阱与注意事项
❌ 陷阱 1:重写 prototype 后丢失 constructor
javascript
编辑
Car.prototype = { /* ... */ }; // 完全覆盖了原来的 prototype 对象
console.log(Car.prototype.constructor === Car); // ❌ false!现在是 Object
✅ 修复方式:手动补回 constructor
javascript
编辑
Car.prototype = {
constructor: Car, // 👈 关键!
name: 'SU7',
drive() { /* ... */ }
};
或者使用
Object.defineProperty避免枚举问题(进阶)。
❌ 陷阱 2:在原型上放引用类型(如数组、对象)
javascript
编辑
function Person(name) {
this.name = name;
}
Person.prototype.hobbies = []; // 危险!
const p1 = new Person('张三');
const p2 = new Person('李四');
p1.hobbies.push('编程');
console.log(p2.hobbies); // ['编程'] ❌ 被污染了!
✅ 正确做法:引用类型应放在构造函数内(实例私有),或在方法中动态创建。
❌ 陷阱 3:误以为 prototype 是实例的属性
javascript
编辑
const car = new Car('红');
console.log(car.prototype); // undefined!
✅ 记住:
- 构造函数才有
.prototype;- 实例只有
.__proto__(或通过Object.getPrototypeOf(obj)获取原型)。
🧩 四、拓展:原型链 vs 类继承(ES6 class)
虽然 ES6 引入了 class 语法糖,但底层仍是原型机制:
javascript
编辑
class Car {
constructor(color) {
this.color = color;
}
drive() {
console.log('Drive!');
}
}
// 等价于 ES5 的构造函数 + prototype 写法
| 对比项 | ES5(prototype) | ES6(class) |
|---|---|---|
| 本质 | 原型链 | 语法糖,底层仍是原型 |
| 可读性 | 较低,需理解原型机制 | 更接近传统 OOP,易读 |
| 灵活性 | 高(可动态修改原型) | 相对封闭 |
| 兼容性 | 全浏览器支持 | 需转译(旧环境) |
✅ 建议:即使使用
class,也务必理解其背后的原型机制!
📌 五、总结要点(复习清单)
- ✅ 构造函数:初始化实例私有属性(如
color)。 - ✅
prototype:存放共享方法/属性,所有实例通过__proto__访问。 - ✅ 原型链:
实例 → 构造函数.prototype → Object.prototype → null。 - ✅ 重写
prototype后记得补constructor。 - ✅ 避免在原型上放可变引用类型(如数组、对象)。
- ✅
class是语法糖,底层仍是原型