本文是一篇学习笔记,整合了 MDN官方文档 和 《JavaScript 高级程序设计(第4版)》(红宝书)中关于继承与原型链的内容。我会尝试用自己的话把这块内容啃下来,如果你也正在学习,希望能对你有所帮助。
限于个人写作,文中若有疏漏,还请不吝赐教。
在开始之前,先记住三个最核心的概念:
prototype:函数才有的属性。它指向一个对象,这个对象将被用作通过该函数创建的实例的"公共祖先"。__proto__(或Object.getPrototypeOf()):实例对象才有的属性。它指向创建这个实例的构造函数的prototype对象。- 原型链:当访问实例的一个属性时,如果实例本身没有,JS 就会顺着
__proto__一层层往上找,直到null。
⚠️ MDN 说明:本文示例中为了简洁会使用
__proto__字面量,实际开发中推荐使用Object.getPrototypeOf()和Object.setPrototypeOf()。
关于用原型链实现"继承",各种方案有一个清晰的递进关系:
- 纯原型链:只能继承方法,不能继承属性(属性会共享) → 不够用
- 盗用构造函数:能继承属性,但不能继承方法 → 也不够用
- 组合继承:两者都用,属性不共享、方法能共用 → 但调用了两次父类构造函数(小瑕疵)
- 寄生式组合继承:修复了"调用两次"的问题 → 最终完美方案,ES6
class底层就是这个
思考注:这个递进关系是红宝书第8.3节的主线。如果你感到混乱,可以先抓住这条线,MDN的例子可以作为补充理解,不必强求一次看完。
基于原型链的继承
继承属性
JavaScript 对象是一个动态的"属性包"(称为自有属性)。每个对象都有一条指向原型对象的内部链。当试图访问对象的属性时,不仅在该对象上查找,还会在其原型、原型的原型上继续查找,直到找到匹配的属性或到达原型链的末尾(null)。
来看 MDN 提供的一个基础例子:
const o = {
a: 1,
b: 2,
// __proto__ 设置了 [[Prototype]]。这里指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
},
};
// 原型链:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null
console.log(o.a); // 1 (自有属性)
console.log(o.b); // 2 (自有属性遮蔽了原型上的 b)
console.log(o.c); // 4 (原型上的属性)
console.log(o.d); // undefined (找不到)
备注:规范中使用
obj.[[Prototype]]表示对象的原型。我们可以通过Object.getPrototypeOf()和Object.setPrototypeOf()来访问和修改它。__proto__是众多 JavaScript 引擎实现的非标准访问器,日常开发中推荐使用标准方法。不要将对象的
[[Prototype]]与函数的prototype属性搞混。函数的prototype属性只在该函数作为构造函数时才有意义,它指向通过new创建的所有实例的[[Prototype]]。
再看一个原型链更长的例子:
const o = {
a: 1,
b: 2,
__proto__: {
b: 3,
c: 4,
__proto__: {
d: 5,
},
},
};
// 原型链:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null
console.log(o.d); // 5
我的理解:这有点像其他语言(如 Java、C++)中的继承链,子类可以访问父类的属性。但 JavaScript 的原型链更灵活、更动态,可以在运行时修改。
接下来看红宝书中关于"构造函数 + 原型链"的例子,这是在实际开发中更常见的场景:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 核心:继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
这里的搜索过程是这样的:
- 实例
instance自身没有getSuperValue - 查找
SubType.prototype(它是SuperType的一个实例),也没有 - 继续查找
SuperType.prototype,找到了这个方法
因为 SuperType 继承了 Object,所以 instance 也可以调用 toString() 等方法。
方法继承与 this 绑定
在 JavaScript 中,方法本质上就是作为属性值的函数。继承函数时,有一个非常重要的特性:this 始终指向当前调用该方法的对象,而不是定义该方法的原型对象。
const parent = {
value: 2,
method() {
return this.value + 1;
},
};
console.log(parent.method()); // 3 (this 指向 parent)
const child = {
__proto__: parent,
};
console.log(child.method()); // 3 (this 指向 child,child.value 来自原型)
child.value = 4; // 在 child 上添加自有属性,遮蔽了原型上的 value
console.log(child.method()); // 5 (this.value 现在是 4)
纯原型链的问题(红宝书核心观点)
原型链虽然是实现继承的强大工具,但它有两个致命问题,导致它基本不会被单独使用。
问题一:引用值会被所有实例共享
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType(); // colors 变成了原型上的共享属性
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
let instance2 = new SubType();
console.log(instance2.colors); // ["red", "blue", "green", "black"] (被污染了)
问题二:无法在实例化时给父类型的构造函数传参
因为 SubType.prototype = new SuperType() 是一次性的赋值,我们无法在每次创建 SubType 实例时,动态地向 SuperType 传递不同的参数。
以上是 ES5 的写法,意在说明"纯原型链"的缺陷。后面我们会看到如何通过"组合继承"来解决这些问题。
构造函数
我们先理解一下"为什么需要构造函数"。
假设我们要创建多个盒子,每个盒子都有一个 value 和一个获取该值的 getValue 方法。一种低效的做法是:
const boxes = [
{ value: 1, getValue() { return this.value; } },
{ value: 2, getValue() { return this.value; } },
{ value: 3, getValue() { return this.value; } },
];
// 问题:每个盒子都有一个相同的 getValue 函数,浪费内存
更好的做法是把 getValue 提取到原型上:
const boxPrototype = {
getValue() {
return this.value;
},
};
const boxes = [
{ value: 1, __proto__: boxPrototype },
{ value: 2, __proto__: boxPrototype },
{ value: 3, __proto__: boxPrototype },
];
但手动绑定 __proto__ 还是很麻烦。构造函数的作用就是:自动为 new 创建的对象设置 [[Prototype]]。
// 构造函数
function Box(value) {
this.value = value;
}
// 在 Box.prototype 上定义方法,所有实例共享
Box.prototype.getValue = function () {
return this.value;
};
const boxes = [new Box(1), new Box(2), new Box(3)];
用 ES6 的 class 语法糖重写:
class Box {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
注意:
class本质上是构造函数的语法糖。你仍然可以修改Box.prototype来影响所有实例。理解原型机制,对于深入理解class至关重要。