讲解

JavaScript 的继承不靠类复制,而靠原型链委托:每个对象内部都有一个指向另一个对象的链接(原型,可通过 Object.getPrototypeOf 读取),读取属性时对象自身没有就沿原型向上找,再没有就找原型的原型,直到链顶端的 Object.prototype,再往上是 null,整条链找不到就返回 undefined。方法调用的好处是共享——一千个实例共用原型上的同一个方法,而不是各存一份。

函数有一个 prototype 属性(箭头函数没有):用 new 调用函数时,新对象的原型会被设为 函数.prototype,这就是构造函数模式。class 语法(下一章)本质上就是这套原型机制的语法糖,class 里写的方法其实都挂在构造函数的 prototype 上。理解原型能解释很多现象:为什么 [1,2,3].map 能用(Array.prototype 上有 map)、为什么几乎所有对象都有 toString(来自 Object.prototype)、instanceof 为什么判断的是「构造函数的 prototype 是否在对象的原型链上」。

现代代码很少直接操作原型(Object.create、Object.setPrototypeOf 用得少),但读源码、调试、理解框架时绕不开。两个实践建议:用 Object.hasOwn 区分自身属性和原型属性;不要修改内置对象的原型(给 Array.prototype 加方法看似方便,会污染所有数组并与未来标准冲突)。

示例

// 构造函数 + prototype:方法共享
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return this.name + ' 发出叫声';
};
const dog = new Animal('狗');
const cat = new Animal('猫');
console.log(dog.speak(), cat.speak());
console.log('方法共享:', dog.speak === cat.speak); // true,同一个函数

// 原型链:实例 → Animal.prototype → Object.prototype → null
console.log('原型是 Animal.prototype:', Object.getPrototypeOf(dog) === Animal.prototype);
console.log('链顶是 Object.prototype:', Object.getPrototypeOf(Animal.prototype) === Object.prototype);
console.log('再往上:', Object.getPrototypeOf(Object.prototype)); // null

// 沿链查找:dog 自身没有 toString,来自 Object.prototype
console.log('toString 来自链上:', typeof dog.toString);

// instanceof 判断原型链
console.log(dog instanceof Animal, dog instanceof Object, dog instanceof Array);

// 自身属性 vs 原型属性
console.log('name 是自身属性:', Object.hasOwn(dog, 'name'));
console.log('speak 是原型属性:', Object.hasOwn(dog, 'speak'));

// class 是原型的语法糖
class Bird {
  fly() {
    return '飞';
  }
}
console.log('class 方法在 prototype 上:', typeof Bird.prototype.fly);

常见坑

  • 混淆 proto 与 prototype:prototype 是函数的属性(当构造函数用的模板);proto 是对象的原型链接(已废弃写法,用 Object.getPrototypeOf/setPrototypeOf)。前者是「图纸」,后者是「实例的血缘」。
  • 把方法挂在实例上:构造函数里写 this.speak = function(){} 会让每个实例各存一份函数,浪费内存且方法不再共享。方法放 prototype(或用 class)。
  • 给内置原型打补丁:Array.prototype.xxx = ... 会影响整个页面所有数组,第三方库冲突、未来标准撞名都是灾难。
  • 原型链查找遮蔽:实例上赋值 dog.speak = ... 不会改原型,而是在实例上新建同名属性「遮蔽」原型方法,之后的调用走实例属性——容易和「修改原型」混淆。

小结

属性查找沿原型链逐级向上;构造函数的 prototype 成为实例原型,class 是其语法糖;方法共享靠原型,自身与原型属性用 Object.hasOwn 区分。下一节学习现代的 class 语法。