1. 原型链 此种继承方式的基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Parent ( ) { this .prop = true } Parent.prototype.getProp = function ( ) { return this .prop } function Son ( ) { this .subProp = false } Son.prototype = new Parent() Son.prototype.getSubProp = function ( ) { return this .subProp } let instance = new Son console .log(instance)
输出如下:
使用此方法实现要注意一点:若在重写原型链之前创建对象,则这个对象不会拥有后面重写原型链之后的方法和属性,我们看下一下下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Parent ( ) { this .prop = true } Parent.prototype.getProp = function ( ) { return this .prop } function Son ( ) { this .subProp = false } let s = new Son() Son.prototype = new Parent() Son.prototype.getSubProp = function ( ) { return this .subProp } let instance = new Son console .log(s, '重写原型链之前' ) console .log(instance, '重写原型链之后' )
输出如下:
可以很明显的看到,重写原型链之前创建的对象s的原型并不会属性prop和方法getSubProp,所以,如果你要用此方法来实现继承,请务必在重写原型链之后再创建新对象。
原型链的问题: (1)原型链实现继承,最主要的问题来自包含引用类型值的原型,请看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Parent ( ) { this .colors = ['red' , 'blue' ] } function Son ( ) { } Son.prototype = new Parent() let s1 = new Son() s1.colors.push('green' ) console .log(s1) let s2 = new Son() console .log(s2)
输出如下:
可以很明显的看到,第二次创建的实例s2的原型中的属性colors中也有了”green”。其实,引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是再原型对象中定义属性的原因。
(2)原型链的第二个问题是:再创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
基于以上两点,我们一般不建议使用原型链的方式实现继承,实际上大多数人也不会单独采用这种方式。
2. 借用构造函数 在解决原型中包含引用类型值带来问题的过程中,开发人员开始使用一种叫做“借用构造函数”的技术(有时候也叫伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数内部调用超类型构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Parent ( ) { this .list = ['football' , 'baseball' ] } function Son ( ) { Parent.call(this ) } let instance1 = new Son() instance1.list.push('tennis' ) console .log(instance1) let instance2 = new Son() console .log(instance2)
输入如下:
可以很明显的看到,实例中的引用类型值不再互相影响,由此解决原型链实现继承带来的引用类型值的问题。此外,相比原型链而言,借用构造函数还有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。请看下面代码:
1 2 3 4 5 6 7 8 9 10 11 function Parent (name ) { this .name = name } function Son ( ) { Parent.call(this , 'Tom' ) this .age = 15 } let instance = new Son() console .log(instance)
输出如下:
这里我们要提醒各位的是,为了确保超类型构造函数不会重写子类型的属性,所以我们建议,请在调用超类型构造函数之后,再添加子类型中定义的属性。
借用构造函数的问题: 很明显的可以发现,这种模式实现的继承,子类型无法获得超类型的原型中定义的属性和方法。基于上述考虑,我们一般也很少单独使用此种方式实现继承。
3. 组合式继承 组合继承,有时候也叫伪经典继承,指的是通过将原型链和借用构造函数的技术组合在一起,取二者之所长实现继承的一种技术。其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。由此,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。请看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function superType (name ) { this .name = name this .colors = ['red' , 'green' , 'blue' ] } superType.prototype.sayName = function ( ) { console .log(this .name) } function subType (name, age ) { superType.call(this , name) this .age = age } subType.prototype = new superType() subType.prototype.constructor = subType subType.prototype.sayAge = _ => {console .log(this .age)} let sub1 = new subType('Tom' , 15 ) console .log(sub1)
输出如下:
组合式继承的问题: 组合式继承虽然融合了原型链和借用构造函数两者的优势,解决了二者单独使用时带来的问题,但是此种方式也不是完美无缺的。我们观察上述输出,不难发现,此种方式实现继承的过程中,调用了两次超类型构造函数;一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。进而就出现了,就上述例子来说,可以发现超类型构造函数中的属性colors和name,既出现在子类实例属性中,也出现在子类原型链上。
4. 原型式继承 利用ES5新增的方法Object.create()实现原型式继承,这个方法接收两个参数:第一个参数用于作为新对象的原型,第二个参数(可选)用于为新对象定义额外的属性;其中,第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的属性描述符定义的。
1 2 3 4 5 6 let person = { area : 'China' , list : ['football' , 'baseball' ] } let p = Object .create(person) console .log(p)
输入如下:
使用此种方式实现继承,所带来的问题和原型链方式一样,同样是引用类型值的问题。
5. 寄生式继承 此种方式的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。请看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function create (obj ) { let clone = Object .create(obj) clone.sayName = function ( ) { console .log('Hello world' ) } return clone } let person = { name : 'Tom' , list : ['baseball' , 'football' ] } let p = create(person) p.sayName()
此种方式与构造函数模式类似,无法做到函数复用而降低效率。
6. 寄生组合式继承 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。此种方式弥补了组合式继承方式带来的缺陷,请看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function superType (name ) { this .name = name this .colors = ['red' , 'green' , 'blue' ] this .pi = function ( ) { console .log(123 ) } } superType.prototype.sayName = function ( ) { console .log(this .name) } function subType (name, age ) { superType.call(this , name) this .age = age } subType.prototype = Object .create(superType.prototype) subType.prototype.constructor = subType subType.prototype.sayAge = _ => {console .log(this .age)} let sub1 = new subType('Tom' , 15 ) console .log(sub1)
输出如下:
可以很明显的看出,此种方式只调用一次超类型构造函数,因此避免了在原型链上创建多余的属性。
总结 业界普遍认为寄生组合式继承是引用类型最理想的继承模式。