JavaScript 原型链继承的工作原理基于 JavaScript 的原型系统,这是一种独特的继承机制,与传统的类继承(如 Java、C++)有本质区别。以下是其核心原理的详细解释:
1. 原型系统的基本概念
1.1 每个对象都有一个内部原型([[Prototype]])
- 每个对象(除
Object.prototype外)都有一个隐藏的内部属性[[Prototype]],指向其原型对象。 - 原型对象本身也是对象,因此也有自己的原型,以此类推,直到最终的原型为
Object.prototype(其[[Prototype]]为null)。
1.2 prototype 属性与构造函数
- 函数作为构造函数时,会自动拥有一个
prototype属性,指向一个普通对象(称为 “原型对象”)。 - 这个原型对象默认包含一个
constructor属性,指向构造函数本身。
function Animal() {}
console.log(Animal.prototype); // { constructor: Animal }
1.3 实例与原型的关联
- 当使用
new关键字创建对象时,实例的[[Prototype]]会指向构造函数的prototype。 - 通过
__proto__(非标准但被广泛支持)或Object.getPrototypeOf()可以访问这个内部原型。
const dog = new Animal();
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
2. 原型链的形成
当访问一个对象的属性或方法时:
-
JavaScript 首先检查对象本身是否存在该属性。
-
如果不存在,则沿着
[[Prototype]]向上查找,直到找到该属性或到达原型链的终点(Object.prototype)。 -
如果最终仍未找到,返回
undefined。
示例:
function Animal() {
this.type = "animal";
}
Animal.prototype.speak = function() {
console.log("I'm an animal");
};
function Dog() {
this.breed = "dog";
}
// 关键步骤:将 Dog 的原型设置为 Animal 的实例
Dog.prototype = new Animal();
const myDog = new Dog();
console.log(myDog.type); // "animal"(从 Animal 实例继承)
console.log(myDog.breed); // "dog"(自身属性)
myDog.speak(); // "I'm an animal"(从 Animal.prototype 继承)
原型链结构:
myDog → Dog.prototype (Animal 实例) → Animal.prototype → Object.prototype → null
3. 原型链继承的本质
通过将子类(如 Dog)的原型设置为父类(如 Animal)的实例,实现以下效果:
- 子类实例继承父类的属性和方法:
子类实例可以访问父类实例的属性(如type)和父类原型的方法(如speak)。 - 共享原型上的方法:
所有子类实例共享父类原型上的方法,节省内存。 - 动态性:
对父类原型的修改会立即反映在所有子类实例上(即使实例已创建)。
4. 原型链的终点
- 所有对象的原型链最终都会指向
Object.prototype。 Object.prototype的原型为null,标志着原型链的结束。
console.log(Object.getPrototypeOf(Object.prototype)); // null
5. 与其他继承方式的对比
5.1 与类继承的区别
- 类继承:基于 “类” 的复制,每个实例独立拥有属性和方法。
- 原型链继承:基于 “委托”,实例通过原型链访问共享属性和方法。
5.2 与构造函数继承的区别
- 构造函数继承:通过在子类构造函数中调用父类构造函数(如
Animal.call(this)),每个实例独立拥有父类属性。 - 原型链继承:子类实例共享父类实例的属性(可能导致引用类型共享问题)。
6. 原型链继承的优缺点
优点:
- 实现简单,直接通过原型关联。
- 方法共享高效,节省内存。
- 动态性:原型修改即时生效。
缺点:
- 引用类型共享问题:
父类实例的引用类型属性(如数组、对象)会被所有子类实例共享。
function Animal() {
this.friends = []; // 引用类型属性
}
const dog1 = new Animal();
const dog2 = new Animal();
dog1.friends.push("Bob");
console.log(dog2.friends); // ["Bob"](意外共享)
- 无法向父类构造函数传参:
子类实例化时无法为父类构造函数提供参数。
7. 现代替代方案
为解决原型链继承的问题,现代 JavaScript 推荐使用:
- 组合继承(结合原型链和构造函数继承):
function Animal(name) {
this.name = name; // 每个实例独立拥有
}
function Dog(name) {
Animal.call(this, name); // 构造函数继承(解决参数传递和引用类型共享)
}
Dog.prototype = Object.create(Animal.prototype); // 原型链继承(共享方法)
- ES6 Class 语法:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`I'm ${this.name}`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类构造函数
}
}
总结
JavaScript 原型链继承的核心是通过原型对象的层级关系实现属性和方法的共享。当访问一个对象的属性时,JavaScript 会自动沿着原型链查找,直到找到或返回 undefined。这种机制虽然灵活,但存在引用类型共享和参数传递的问题,实际开发中通常需要结合其他继承模式或使用 ES6 Class 语法。