Skip to content

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 引擎的查找规则是:
    1. 首先,在对象自身查找。
    2. 如果找不到,就沿着__proto__这条链,去它的原型对象上查找。
    3. 如果还找不到,就继续沿着原型对象的__proto__向上查找,直到链的终点。
    4. 原型链的终点是 Object.prototype.__proto__ 的原型,也就是 null
  • 通俗比喻: 你(一个对象实例)想找一样东西(一个属性)。
    1. 你先在自己口袋里找(对象自身)。
    2. 找不到,你去问你爸爸(对象的原型),看他有没有。
    3. 你爸爸也没有,他就去问你爷爷(原型的原型)。
    4. 这个“寻根问祖”的过程一直持续到最老的祖先(Object.prototype),如果连他都没有,那就说明家里真没这东西。

原型链图示:

以上面的car1为例,它的原型链是: car1 ---> Car.prototype ---> Object.prototype ---> null

当你执行 car1.toString() 时:

  1. car1 自身没有 toString 方法。
  2. JS 沿着car1.__proto__找到 Car.prototypeCar.prototype上也没有 toString
  3. 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)
本质都是基于原型链都是基于原型链
语法过程式、分散。需要手动处理 prototypeconstructorcall声明式、内聚。用 class, constructor, extends, super 等关键字封装。
可读性较差,对新手不友好。极佳,更接近传统面向对象语言,易于理解和维护。
实现细节开发者需要深刻理解原型链的每一个步骤才能正确实现。关键字自动处理了原型链的连接、构造函数的指向修正等繁琐工作。
Hoisting函数声明会被提升,可以先使用后定义。class 声明不会被提升,必须先声明再使用。
严格模式默认不使用。class 内部的代码默认运行在严格模式 (strict mode) 下。