深入理解 JavaScript 原型:从“拿下小米 SU7”到掌握原型链本质

63 阅读3分钟

🚗 一、引子:如何“拿下”一辆小米 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 原型

角色职责存放内容内存
构造函数初始化实例每个实例独有的数据 (如 colorowner每个实例一份
原型(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

完整原型链:

fba534ea3da9a5c8b4c390ef606e1dde.png ✅ 验证:

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!

🌟 真正的高手,不仅会用轮子,更懂得轮子是怎么转的

欢迎点赞、收藏、评论!

如果你觉得这篇文章帮你理清了原型的迷雾,不妨分享给更多正在“造车”的朋友 🚗💨

作者:想要成为糕糕手