JavaScript 原型与原型链的深度解析
我们来聊聊 JavaScript 的“灵魂”——原型 (Prototype)。刚接触时,它可能有些绕,但一旦你理解了它,就会发现这是 JavaScript 一个极其强大和灵活的设计。
1. prototype
(原型) - 对象的“设计蓝图”
- 是什么? 在 JavaScript 中,函数是一个非常特殊的对象。当你创建一个函数时,JS 引擎会自动为这个函数添加一个
prototype
属性,而这个属性本身也是一个对象。我们称之为“原型对象”。 - 有什么用? 这个原型对象的核心用途是存放所有实例共享的属性和方法。
- 通俗比喻: 想象一个“汽车工厂”(这就是我们的构造函数
Car
)。工厂有一个设计蓝图(这就是Car.prototype
)。- 这个蓝图上定义了所有汽车共有的功能,比如“鸣笛()”、“加速()”。把这些方法放在蓝图上,就不需要为每一辆生产出来的汽车都单独安装一套鸣笛系统,大大节省了成本和资源(内存)。
- 每辆从工厂生产出来的汽车(实例
car1
,car2
),都有自己独特的车架号、颜色(实例自身的属性,存放在this
上)。 - 但当你让
car1
鸣笛时,它会去查找那份共享的“设计蓝图”来执行鸣笛动作。
代码示例:
javascript
// 1. 创建一个构造函数 (汽车工厂)
function Car(color) {
// 这是实例自身的属性 (每辆车都不同)
this.color = color;
this.engine = 'V8';
}
// 2. 在函数的 prototype 对象上定义共享的方法 (设计蓝图)
Car.prototype.honk = function () {
console.log('滴滴!');
};
Car.prototype.drive = function () {
console.log(`这辆${this.color}的、搭载${this.engine}引擎的车开动了。`);
};
// 3. 创建实例 (生产两辆车)
const car1 = new Car('红色');
const car2 = new Car('黑色');
car1.drive(); // 输出: 这辆红色的、搭载V8引擎的车开动了。
car2.drive(); // 输出: 这辆黑色的、搭载V8引擎的车开动了。
// 验证共享特性
console.log(car1.drive === car2.drive); // 输出: true。证明它们使用的是内存中同一个函数。
console.log(car1.hasOwnProperty('color')); // 输出: true。color是实例自身的属性。
console.log(car1.hasOwnProperty('drive')); // 输出: false。drive方法不在实例自身,而在原型上。
2. 原型链 (Prototype Chain) - 对象的“家谱”
- 是什么? 每个 JavaScript 对象(除了少数特例)在创建时,都会被关联到另一个对象,这个关联就是通过一个内部的、隐藏的属性,在 ES6 之前我们俗称为
__proto__
来实现的。这个__proto__
指向了其构造函数的prototype
对象。当多个对象的原型连接在一起,就形成了一条链式结构,这就是原型链。 - 有什么用? 当你试图访问一个对象的属性或方法时,JavaScript 引擎的查找规则是:
- 首先,在对象自身查找。
- 如果找不到,就沿着
__proto__
这条链,去它的原型对象上查找。 - 如果还找不到,就继续沿着原型对象的
__proto__
向上查找,直到链的终点。 - 原型链的终点是
Object.prototype.__proto__
的原型,也就是null
。
- 通俗比喻: 你(一个对象实例)想找一样东西(一个属性)。
- 你先在自己口袋里找(对象自身)。
- 找不到,你去问你爸爸(对象的原型),看他有没有。
- 你爸爸也没有,他就去问你爷爷(原型的原型)。
- 这个“寻根问祖”的过程一直持续到最老的祖先(
Object.prototype
),如果连他都没有,那就说明家里真没这东西。
原型链图示:
以上面的car1
为例,它的原型链是: car1
---> Car.prototype
---> Object.prototype
---> null
当你执行 car1.toString()
时:
car1
自身没有toString
方法。- JS 沿着
car1.__proto__
找到Car.prototype
,Car.prototype
上也没有toString
。 - JS 继续沿着
Car.prototype.__proto__
找到Object.prototype
,在这里找到了toString
方法,并执行它。
3. 利用原型链实现继承
继承的本质就是让一个构造函数的原型,去关联另一个构造函数的原型,从而把“家谱”给接上。
代码示例:创建一个 Truck
(卡车) 来继承 Car
javascript
// 父类 (已在上面定义)
// function Car(...) { ... }
// 子类构造函数
function Truck(color, payload) {
// 1. 继承实例属性: 使用 .call() 借用父类的构造函数
// 这相当于让Car工厂帮你完成一部分基础制造工作
Car.call(this, color);
// Truck自己的实例属性
this.payload = payload;
}
// 2. 继承原型方法: 核心步骤!连接原型链
// 创建一个以 Car.prototype 为原型的新对象,并赋值给 Truck.prototype
// 这样既实现了继承,又避免了直接修改父类原型
Truck.prototype = Object.create(Car.prototype);
// 3. 修复构造函数指向: 这是一个重要的收尾工作
// 因为上一步重写了原型,需要把 constructor 指针修正回 Truck 自己
Truck.prototype.constructor = Truck;
// 4. 在子类原型上添加自己的方法
Truck.prototype.unload = function() {
console.log(`卸下${this.payload}吨的货物。`);
};
// 创建实例
const myTruck = new Truck('蓝色', 10);
myTruck.drive(); // -> "这辆蓝色的、搭载V8引擎的车开动了。" (继承自Car)
myTruck.honk(); // -> "滴滴!" (继承自Car)
myTruck.unload(); // -> "卸下10吨的货物。" (Truck自己的方法)
// myTruck的原型链:
// myTruck ---> Truck.prototype ---> Car.prototype ---> Object.prototype ---> null```
---
### 4. 与 ES6 `class` 的对比
ES6的 `class` 关键字的出现,并不是发明了一套新的继承机制。它只是对JavaScript已有的**原型继承提供了一层更优雅、更易于理解的“语法糖”**。
**`class` 就是一件华丽的外衣,它里面的身体,依然是原型。**
**使用 `class` 重写上面的继承**:
```javascript
// 父类
class Car {
constructor(color) {
this.color = color;
this.engine = 'V8';
}
honk() {
console.log('滴滴!');
}
drive() {
console.log(`这辆${this.color}的、搭载${this.engine}引擎的车开动了。`);
}
}
// 子类
class Truck extends Car {
constructor(color, payload) {
// super() 做了两件事:
// 1. 相当于 Car.call(this, color),调用父类构造函数
// 2. 必须在 this 之前调用
super(color);
this.payload = payload;
}
unload() {
console.log(`卸下${this.payload}吨的货物。`);
}
}
const myTruck = new Truck('蓝色', 10);
myTruck.drive();
myTruck.unload();
console.log(myTruck instanceof Car); // true
console.log(myTruck instanceof Truck); // true
区别与联系总结
特性 | 原型继承 (ES5) | class 继承 (ES6) |
---|---|---|
本质 | 都是基于原型链 | 都是基于原型链 |
语法 | 过程式、分散。需要手动处理 prototype 、constructor 和 call 。 | 声明式、内聚。用 class , constructor , extends , super 等关键字封装。 |
可读性 | 较差,对新手不友好。 | 极佳,更接近传统面向对象语言,易于理解和维护。 |
实现细节 | 开发者需要深刻理解原型链的每一个步骤才能正确实现。 | 关键字自动处理了原型链的连接、构造函数的指向修正等繁琐工作。 |
Hoisting | 函数声明会被提升,可以先使用后定义。 | class 声明不会被提升,必须先声明再使用。 |
严格模式 | 默认不使用。 | class 内部的代码默认运行在严格模式 (strict mode) 下。 |