拿到小米 SU7?不,是理解 JavaScript 原型链:从 Car 构造函数说起

52 阅读3分钟

一句话总结:在 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 是每个实例独有的;
  • namedrive() 等定义在 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 是语法糖,底层仍是原型

image.png