继承
很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在ECMAScript中是不可能的,因为函数没有签名。实现继承是ECMAScript唯一支持的继承方式,而这主要是通过原型链实现的。继承有以下六种实现方式:原型链、盗用构造函数、组合继承、原型式继承、寄生式继承、寄生式组合继承
参考书目《JavaScript高级程序设计(第4版)》
1、原型链
原型链继承方式
在此之前需先明确原型与原型链的概念 原型与原型链_问号制造商的博客-CSDN博客
以下述代码为例,继承的关键在于Son没有使用默认原型,而是将其换成了一个新的对象(Son.prototype=new Father())
原型链继承代码
/**原型链继承代码模式**/
function Father(){
this.lastName='Zhang'
this.firstName='baba'
}
Father.prototype.getLastname=function(){
return this.lastName
}
function Son(){
this.firstName='San'
}
//Son继承Father
Son.prototype=new Father()
Son.prototype.getName=function(){
return this.lastName+this.firstName
}
let instance1=new Son()
console.log(instance1.getName()) //ZhangSan
原型链继承图解
原型链搜索机制
在读取实例上的属性时,会先在实例上搜索,若未找到,则会继承搜索实例的原型,若还未找到,则继续向上,搜索原型的原型。
缺点
原型中包含的引用值会在所有实例间共享
子类型在继承时不能给父类型的构造函数传参
2、盗用构造函数
解决原型包含引用值导致的继承问题
解决子类型不能给父类型传参的问题
基本思路
在子类构造函数中调用父类构造函数,使用call方法改变this指向,以新创建的对象为上下文执行构造函数
function Father(name){
this.name=name
this.colors=['red','green','blue']
}
function Son(){
//继承Father
Father.call(this,'zhangsan')//谁调用this就指向谁
this.age=18
}
let instance1=new Son()
instance1.colors.push('pink')
console.log(instance1.colors,instance1.name) //['red','green','blue','pink'] 'zhangsan'
let instance2=new Son()
console.log(instance2.colors) //['red','green','blue']
通过使用call方法,Father构造函数在为Son创建的对象的上下文中执行了,这相当于在新的Son对象上运行了Father()函数中的所有初始化代码,所以每个实例都会有自己的name、age和colors属性
缺点
必须在构造函数中定义方法,因此函数不能重用
子类不能访问父类原型上定义的方法
3、组合继承
基本思路
组合继承综合了原型链和盗用构造函数,使用原型链继承原型上的属性和方法,使用盗用构造函数继承实例属性
function Father(firstName){
this.lastName='Zhang'
this.firstName=firstName
this.colors=['red','green','blue']
}
Father.prototype.getLastname=function(){
return this.lastName
}
function Son(name){
//盗用构造函数继承Father的属性,子类型向父类型传参
Father.call(this,name)
}
//Son原型链继承Father
Son.prototype=new Father()
Son.prototype.getName=function(){
return this.lastName+this.firstName
}
let instance1=new Son('san')
instance1.colors.push('pink')
console.log(instance1.getName(),instance1.colors) //Zhangsan ['red','green','blue','pink']
let instance2=new Son('ha')
instance1.colors.push('orange')
console.log(instance2.getName(),instance2.colors) //Zhangha ['red','green','blue','orange']
缺点
父类构造函数始终会被调用两次:一次是在创建子类原型时调用,另一次是在子类构造函数中调用
4、原型式继承
不单独创建构造函数实现对象间的信息共享
适用
不需要单独创建构造函数,但仍需要在对象间共享信息的场合
基本思路
object()函数临时创建一个构造函数,将传入的对象赋值给构造函数的原型,返回这个临时类型的一个实例
function object(o){
function F(){}
F.prototype=o
return new F()
}
ECMAScript5新增了Object.create()方法,该方法接收两个参数:作为新对象原型的对象;给新对象定义额外属性的对象(可省略)。在只有一个参数时,Object.create()与上述object()效果相同
function object(o){
function F(){}
F.prototype=o
return new F()
}
let person={
name:'happy',
friends:['joy','candy','amy']
}
let instance1=object(person)
instance1.name='haha'
instance1.friends.push('tony')
console.log(instance1)
let instance2=Object.create(person)
instance2.friends.push('tom')
console.log(instance2)
console.log(person)
本质上object()是对传入的o执行了一次浅复制,因此o对象属性中包含的引用值会在相关对象间共享,上图打印结果体现了这一点
缺点
父对象属性中包含的引用值始终会在相关对象间共享
5、寄生式继承
适用
关注对象,而不在乎类型和构造函数的场景
基本思路
创建一个实现继承的函数,增强对象,然后返回这个对象
//object函數不是寄生式继承所必须的,任何返回新对象的函数都可以在这里使用
function object(o){
function F(){}
F.prototype=o
return new F()
}
function extendsFather(father){
let clone=object(father)
clone.age=20
clone.sayHi=function(){
console.log('Hi~'+clone.name)
}
return clone
}
let person={
name:'happy',
friends:['joy','candy','amy']
}
let instance1=extendsFather(person)
instance1.sayHi() //Hi~happy
缺点
函数难重用
6、寄生式组合继承
解决组合继承中,父类构造函数始终会被调用两次的问题
问题分析
首先简要回顾一下组合继承
function Son(name){
//盗用构造函数继承Father的属性,子类型向父类型传参
Father.call(this,name) //第二次调用Father
}
//Son原型链继承Father
Son.prototype=new Father() //第一次调用Father()
let instance1=new Son('san')
第一次调用Father时,Father的实例属性(lastName、firstName、colors)会成为Son的原型属性,第二次调用Father时,会在新对象上创建Father的实例属性,由于原型链的搜索机制,实例属性会遮蔽原型上的同名属性,这也是为什么 盗用构造函数继承可以解决原型链继承造成的引用值在实例间共享的问题
由此带来的问题是:有两组lastName、firstName、colors属性,一组在实例instance1上(第二次调用Father所致),一组在Son的原型上(第一次调用Father所致)
寄生式组合继承很好的解决了这个问题
基本思路
不通过调用父类构造函数给子类原型赋值(即不使用Son.prototype=new Father()),而是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型
//寄生式组合继承
function Father(firstName){
this.lastName='Zhang'
this.firstName=firstName
this.colors=['red','green','blue']
}
Father.prototype.getLastname=function(){
return this.lastName
}
function Son(name){
Father.call(this,name)
}
function object(o){
function F(){}
F.prototype=o
return new F()
}
function inheritPrototype(son,father){
let prototype=object(father.prototype)
prototype.constructor=son
son.prototype=prototype
}
//Son寄生式继承Father
inheritPrototype(Son,Father)
Son.prototype.getName=function(){
return this.lastName+this.firstName
}
let instance1=new Son('san')
instance1.colors.push('pink')
console.log(instance1)
这里只调用了一次Father(),避免了Son.prototype上不必要也用不到的属性,因此效率更高。寄生式组合继承是引用类型继承的最佳模式