关于 ES5 的六种继承 TL, DR 六种继承方式的总结
原型链继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Parent ( ) {}Parent .prototype .age = 18 Parent .prototype .getName = function ( ) { return this .name } function Child (name ) { this .name = name } Child .prototype = new Parent ()const child = new Child ("leo" )console .log (child.age ) console .log (child.getName ())
分析: 所有的实例的原型链属性 [[Prototype]]
都指向同一个原型对象(Parent.prototype
),而且这是一个引用类型 。从而导致数据会被共享
构造函数继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Parent (name ) { this .name = name this .food = ["水果" ] } Parent .prototype .getName = function ( ) { return this .name } function Child (name ) { Parent .call (this , name) } const child1 = new Child ("leo" )child1.food .push ("apple" ) const child2 = new Child ("bruce" )console .log (child1.name ) console .log (child1.food ) console .log (child2.food ) console .log (child1.getName ())
分析: 使用构造函数继承,可以避免共享原型对象的情况,但是却不能继承父类的方法了。因为父类方法是挂载到原型对象上的,调用构造函数并不会发生原型链指向改动
组合继承(常用) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function Parent (name ) { this .name = name this .colors = ["res" , "blue" , "green" ] } Parent .prototype .getName = function ( ) { console .log (this .name ) } function Child (name ) { Parent .call (this , name) } Child .prototype = new Parent ()Child .prototype .constructor = Child const child1 = new Child ("foo" )child1.colors .push ("black" ) child1.getName () console .log (child1.name ) console .log (child1.colors ) const child2 = new Child ("bar" )child2.getName () console .log (child2.name ) console .log (child2.colors ) console .log (child2.__proto__ .colors )
分析: 解决了共享原型对象、不能继承父类方法的问题,但是由于 call()
和 new Parent()
调用了两次父构造函数,导致 __proto__
和 实例上有重复的属性
原型式继承 1 2 3 4 5 6 7 8 9 10 11 12 function Parent (name ) { this .name = name this .colors = ["res" , "blue" , "green" ] } let Child = new Parent ()let child1 = Object .create (Child )let child2 = Object .create (Child )child1.colors .push ("花椒" ) console .log (child1.colors ) console .log (child2.colors )
分析: 原型式继承主要是通过 Object.create()
来将实例 child1.__proto__
指向 Child
(即 child1.__proto__ === Child
)
但这也会面临所有实例的原型链共享同一个对象(Child
)的问题
寄生式继承 与原型式继承比较接近的一种继承方式是寄生式继承。寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
——《JavaScript 高级程序设计》
1 2 3 4 5 6 7 8 function parasiticInheritance (object ) { const clone = Object .create (object) clone.sayHi = function ( ) { console .log ('hi' ) } return clone }
分析: 寄生继承核心实现是完成继承 + 给实例添加方法
寄生式组合继承(常用) 组合继承其实也存在效率问题,最主要的效率问题就是父类构造函数始终会被调用两次:一次是在子类构造函数中调用,一次是创建子类原型时调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
——《JavaScript 高级程序设计》
1 2 3 4 5 function inherit (child, parent ) { let prototype = Object .create (parent.prototype ) prototype.constructor = child child.prototype = prototype }
这个 inherit 函数实现了寄生式组合继承的核心逻辑
inherit()
接收两个参数,子类构造函数和父类构造函数,在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype
对象设置 constructor
属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
1 2 3 4 5 6 7 8 9 10 function Parent (name ) { this .name = name this .likeFood = ["水果" , "鸡" , "烤肉" ] } function Child (name ) { Parent .call (this , name) } inherit (Child , Parent )
这里只调用了一次 Parent 构造函数,避免了在 Child.prototype
上绑定不必要的属性(像组合继承那样),而且原型链依然保持不变,因此 instanceof
和 isPrototypeOf()
正常有效,寄生式组合继承可以算是引用类型继承的最佳模式
引用文章