先记住这个答案
子类实例要能调用父类方法,必须让 Child.prototype 的原型指向 Parent.prototype。早年没有 Object.create,唯一能造出这样一个对象的办法就是 new Parent(),所以才有了 Child.prototype = new Parent() 的写法。它的问题有两个:父构造函数在定义继承时就被执行了一次,可能带副作用或缺参数;父类在 this 上定义的属性变成了原型属性,所有子实例共享同一份。改进办法是 Child.prototype = Object.create(Parent.prototype),只接原型链不跑构造函数,再在子构造函数里 Parent.call(this) 初始化实例属性。
new Parent()会接上父类原型链- 副作用:父构造函数多跑一次,引用类型属性被共享
Object.create接链但不执行父构造器- 改完原型要把
constructor指回子类
寄生组合继承:ES5 的标准答案
改进分两半做:实例属性交给 Parent.call(this, name),每次 new Child 都在新对象上跑一遍父构造函数,属性各有一份;方法继承交给 Object.create(Parent.prototype),它返回一个空对象,原型指向父类原型,不会执行 Parent。
- 三行代码接好继承
- 写法是这样的:子构造函数里调用
Animal.call(this, name);然后Cat.prototype = Object.create(Animal.prototype);最后用Object.defineProperty(Cat.prototype, 'constructor', { value: Cat, writable: true, configurable: true })把constructor补回来。用defineProperty而不是直接赋值,是为了让它和原生的一样不可枚举,for...in时不会冒出来。 这样new Cat('mimi').say()返回'I am mimi',两个Cat实例的tags互不影响,constructor也指回了Cat。
为什么说组合继承还不够
很多教材讲的「组合继承」是 Parent.call(this) 加 Child.prototype = new Parent()。实例属性确实不共享了,因为实例自己的 tags 会遮住原型上那个。但父构造函数一共跑了两次,原型上还留着一份没用的 name 和 tags,占内存,调试时看着也乱。寄生组合继承只是把第二步换成 Object.create,就把这两个问题一起去掉了。
容易答错的地方
- 以为
Child.prototype = Parent.prototype更简单 - 这样两个构造函数共用同一个原型对象,给
Child.prototype加方法等于给Parent.prototype加方法,父类实例也能调子类方法,继承关系直接乱了。 - 忘了修
constructor - 不修的话
child.constructor指向父类。平时影响不大,但有些库会用obj.constructor创建同类对象,比如克隆工具,这时会造出父类实例。 - 以为
Object.create会调用构造函数 Object.create(proto)只是造一个原型为proto的空对象,完全不碰proto.constructor,这正是它能避开副作用的原因。
面试官还会怎么问?
ES6 的 class extends 和寄生组合继承有什么区别?
原型链的结构一样。区别有三点:extends 会把 Child.__proto__ 设成 Parent,静态方法也能继承;子类必须先调用 super() 才能用 this,实例由父类构造函数创建,所以能正确继承 Array、Error 这些内置类;class 必须用 new 调用。
没有 Object.create 的环境怎么写?
用一个空的中间函数:function F() {},F.prototype = Parent.prototype,再 Child.prototype = new F()。F 什么都不做,所以没有副作用,这就是 Object.create 的经典 polyfill。
怎么让子类继承父类的静态方法?
ES5 写法里静态方法挂在构造函数本身上,原型链上接不到。可以 Object.setPrototypeOf(Child, Parent),或者把静态方法逐个复制过去,class extends 则会自动处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。