因为JS中没有类的概念,无法像其他语言一样完成“继承”,只能通过一些方式进行模拟,即使是ES6中的class也只是一个语法糖,也是通过ES5来完成的继承。下面介绍JS中的继承方式。
注:下面用类
来指代构造函数
。
构造函数继承
通过使用call
函数在子类B中调用父类A的构造函数,将父类的成员变量和成员函数传给子类
function A(name){
this.name = name
}
A.prototype.GetName=function(){
return this.name
}
function B(){
A.call(this, 'argument')
}
var b = new B()
console.log(b.name) // argument
console.log(b.GetName()) // Uncaught TypeError: b.GetName is not a function
缺点:无法继承父类的原型上的函数和变量,只能继承成员属性和成员变量。
原型链继承
使子类的原型prototype
指向父类的实例对象,这样子类的实例通过原型链访问到父类的属性和函数
function A(name){
this.name = name
this.color = ['red', 'green', 'blue']
}
A.prototype.GetName=function(){
return this.name
}
function B(){
}
B.prototype = new A('nameA')
var b1 = new B()
var b2 = new B()
console.log(b1.color) // ['red', 'green', 'blue']
console.log(b2.color) // ['red', 'green', 'blue']
b1.color.push('gray')
console.log(b2.color) // ['red', 'green', 'blue', 'gray']
缺点:因为子类的原型是父类的实例对象,所以如果父类的实例上有一个引用类型对象,例如上例中的color
,那么所有的子类的实例的color
都是同一个对象,在任何一个对象中修改该属性,其他实例中也会被修改。
组合继承
结合前两种继承方式,
// 父类
function A(name) {
this.name = name;
this.color = ['red', 'blue']
this.conFun = function () {
console.log('super type')
}
}
// A原型添加方法
A.prototype.getName = function () {
return this.name
}
// 子类1
function B(name) {
A.call(this, name);
this.fruit = ['apple', 'banana']
}
// 不使用B.prototype = new A(),保证A的成员属性不会出现在B的原型链上
B.prototype = A.prototype
B.prototype.constructor = B
// 这里B原型添加方法必须在继承之后,否则,该原型中的方法会因为prototype指向改变而消失
B.prototype.getBName = function () {
return 'B:'+this.name
}
// 子类2
function C(name) {
A.call(this, name);
this.fruit = ['apple', 'banana']
}
C.prototype = A.prototype
C.prototype.constructor = C
// 这里C原型添加方法必须在继承之后,否则,该原型中的方法会因为prototype指向改变而消失
C.prototype.getCName = function () {
return 'C:'+this.name
}
var b = new B('bbbname')
var c = new C('cccname')
// B上可以访问getCName,C上可以访问getBName
b.getCName() // C:bbbname
c.getBName() // B:cccname
缺点:这种继承方式规避了前面两种继承方式的缺点,但是仍然有点问题,这种直接将父类的原型(引用类型)赋值给子类的原型,那么所有继承于该父类的子类(可能有多个),他们的原型是同一个对象,多个子类都在原型上添加方法时,这些方法在每一个子类的实例中都可以被访问到。
原型式继承
该继承方式与前面的不太一样,因为这种不需要构造函数,仅仅需要对象就可以了。
var superobj = {
name: 'superobj',
color: ['red', 'blue'],
printname: function(){
console.log(this.name)
}
}
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
// 子对象
var subobj1 = object(superobj)
var subobj2 = object(superobj)
subobj1.color.push('yellow')
console.log(subobj2.color) // ['red', 'blue', 'yellow']
缺点:无法复用,无法在原型上添加方法
寄生式继承
寄生式本质上和原型式没有什么区别,只是封装了一个用来继承的函数,在这个函数中可以对生成的子对象进行一些定制。
var superobj = {
name: 'superobj',
color: ['red', 'blue'],
printname: function(){
console.log(this.name)
}
}
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
function InheritObj(sobj, custumerobj){
var clone = object(sobj)
Object.assign(clone, custumerobj)
return clone
}
// 子对象
var subobj1 = InheritObj(superobj)
var subobj2 = InheritObj(superobj)
subobj1.color.push('yellow')
console.log(subobj2.color) // ['red', 'blue', 'yellow']
寄生组合继承
该方式是目前为止最完美的解决方案,解决了前面出现的问题
这种方式是构造函数继承+寄生式继承的结合
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
function InheritObject(SubType, SuperType){
var prototype = object(SuperType.prototype)
prototype.constructor = SubType
SubType.prototype = prototype
}
// 父类
function SuperType(){
this.type = 'super'
this.colors = ['red', 'blue', 'yellow']
}
SuperType.prototype.printColors = function(){
console.log(this.colors)
}
// 子类
function SubType(){
// 构造函数继承
SuperType.call(this)
this.type = 'sub'
}
// 寄生继承
InheritObject(SubType, SuperType)
SubType.ptoyotype.printtype = function(){
console.log(this.type)
}
var sub1 = new SubType()
var sub2 = new SubType()
sub1.colors.push('green')
sub1.printColors() // ['red', 'blue', 'yellow', 'green']
sub2.printColors() // ['red', 'blue', 'yellow']
利用ES6的方法改写寄生组合式继承
利用Object.create
和Object.assign
函数
// 父类
function SuperType(){
this.type = 'super'
this.colors = ['red', 'blue', 'yellow']
}
SuperType.prototype.printColors = function(){
console.log(this.colors)
}
// 子类
function SubType(){
// 构造函数继承
SuperType.call(this)
// 继承于多个类的情况
OtherSuperType.call(this)
this.type = 'sub'
}
// 关键点,将父类原型包装一层赋值给子类原型
SubType.prototype = Object.create(SuperType.prototype)
// 混入其他父类的原型属性
Object.assign(SubType.prototype, OtherSuperType.prototype)
SubType.prototype.constructor = SubType
SubType.ptoyotype.printtype = function(){
console.log(this.type)
}
var sub1 = new SubType()
var sub2 = new SubType()
sub1.colors.push('green')
sub1.printColors() // ['red', 'blue', 'yellow', 'green']
sub2.printColors() // ['red', 'blue', 'yellow']
总结
这些继承的方式主要出现的是如下三个问题:
- 如何将
实例属性
和原型上的函数
都继承(原型链继承
和构造函数继承
的问题,组合继承
解决了)。 - 非函数属性出现在原型链上会被所有实例共享,从而出现一个实例修改了,其他实例受影响(原型链继承中,将父类实例作为子类的原型造成的问题)。
同一个父类
的多个子类原型对象相同
,导致不同子类添加的原型函数会被共享(这是组合继承
、原型式继承
和寄生式继承
的缺点),这是因为直接将父类的原型赋值给子类的原型。
针对每个问题的关键解决方式
原则:构造函数中设置非函数属性,原型中存储函数属性
第一个问题:实例属性必须通过构造函数继承实现,即SuperType.call(this)
,即必须通过构造函数+原型组合完成。
第二个问题:不能将父类的实例赋值给子类的原型。
第三个问题:不能将父类的prototype
直接赋值给子类的prototype
,需要在父类prototype
外层套一个对象包装,再赋值给子类prototype
,这也是寄生组合继承中InheritObject
函数所做的。