学完这篇,你能分清六种 JavaScript 继承写法各自的优缺点,并且能手写出没有明显短板的寄生组合式继承。
第一步:先把三根指针捋清楚
继承的所有问题都源于三个关系没搞明白:
function Foo() {}
const f = new Foo();
f.__proto__ === Foo.prototype; // true
Foo.prototype.constructor === Foo; // true
Foo.prototype.__proto__ === Object.prototype; // true
new 干了四件事:创建空对象 → 把空对象的原型指向构造函数的 prototype → 把 this 绑上去执行构造函数 → 返回对象。属性查找时如果自己身上没有,就沿着 __proto__ 一层层往上找,直到 null,这条链就是原型链。
第二步:原型链继承
function Parent() {
this.name = 'parent';
this.colors = ['red'];
}
Parent.prototype.say = function () { console.log(this.name); };
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const a = new Child(), b = new Child();
a.colors.push('blue');
console.log(b.colors); // ['red', 'blue'] —— 被污染了
优点是原型方法能复用。缺点很明确:引用类型属性被所有实例共享;创建子类实例时没法给父类传参。
第三步:借用构造函数继承
function Child(name) {
Parent.call(this); // 把父类当普通函数调用
this.name = name;
}
用 call 把父类的私有属性复制一份到子类实例上,引用类型不再共享,也能传参了。
注意:这种方式拿不到父类原型上的方法,而且方法写在构造函数里,每个实例都会新建一份函数,内存浪费。
第四步:组合继承(原型链 + 构造函数)
function Child(name) {
Parent.call(this, name); // 第二次调用父类
}
Child.prototype = new Parent(); // 第一次调用父类
Child.prototype.constructor = Child;
属性用构造函数继承、方法用原型链继承,这是 ES5 时代最常见的写法。问题是父类构造函数被调用了两次,子类原型上多挂了一份永远用不到的实例属性。
第五步:原型式继承
function create(o) {
function F() {}
F.prototype = o;
return new F();
}
const obj = create(parentObj);
// 等价于
const obj2 = Object.create(parentObj);
不关心构造函数、只想让一个对象以另一个对象为原型时用它。缺点和原型链继承一样:引用类型共享。
第六步:寄生式继承
在原型式继承外面再包一层,顺手给新对象加方法:
function createAnother(o) {
const clone = Object.create(o);
clone.say = function () { console.log('hi'); };
return clone;
}
思路是把“增强对象”这件事封装成函数。但方法还是每个实例各一份,复用性差,单独用没什么意义,它的价值在于和组合继承结合。
第七步:寄生组合式继承(ES5 最优解)
核心是:不再 new Parent(),而是用 Object.create 直接把子类原型挂到父类原型的副本上。
function inherit(Child, Parent) {
const proto = Object.create(Parent.prototype);
proto.constructor = Child;
Child.prototype = proto;
}
function Child(name) {
Parent.call(this, name); // 父类只调用一次
this.name = name;
}
inherit(Child, Parent);
父类构造函数只执行一次,原型方法正常复用,instanceof 和 constructor 都正确,这就是 ES6 class extends 编译后的样子。
class Child extends Parent {
constructor(name) {
super(name); // 必须在使用 this 之前调用
this.name = name;
}
}
注意:super() 之前访问 this 会直接报 ReferenceError,这是 class 和 ES5 写法最大的行为差异。
六种方式横向对比
| 方式 | 引用共享 | 可传参 | 父构造调用次数 | 方法复用 |
|---|
| 原型链继承 | 是 | 否 | 1 | 好 |
| 构造函数继承 | 否 | 是 | 1 | 差 |
| 组合继承 | 否 | 是 | 2 | 好 |
| 原型式继承 | 是 | 否 | 0 | 好 |
| 寄生式继承 | 是 | 否 | 0 | 差 |
| 寄生组合式继承 | 否 | 是 | 1 | 好 |
注意:改写 Child.prototype 后一定要补 constructor 指向,否则 instanceof 之外的判断(比如 obj.constructor === Child)会出错。
注意:Object.create(null) 创建的对象没有原型链,调不到 hasOwnProperty,需要 Object.prototype.hasOwnProperty.call(obj, key)。
小结
- 原型链查找靠
__proto__ 逐级向上,new 的本质是把实例原型指向构造函数 prototype。
- 前五种写法都各有硬伤:要么引用共享,要么方法不复用,要么父类被调两次。
- 寄生组合式继承是 ES5 的标准答案:
Object.create(Parent.prototype) + Parent.call(this)。
- 现代项目直接写
class extends,但要记得 super() 先于 this。
- 面试手写继承,写寄生组合式并补上
constructor,基本就稳了。