今天 主要来研究一下继承这个东西
继承
共分为六种继承方式:
- 原型链继承
- 盗用构造函数继承
- 组合继承
- 实例继承(原型式继承)
- 寄生式继承
- 寄生式组合继承
原型链继承
原型链继承是ES主要继承方法,其中基本思想就是 通过原型链继承多个引用类型的属性和方法
那么构造函数、原型、实例之间的关系是什么呢?
答:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型,如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。。。这样就在实例与原型之间构造了一条原型链,,,这就是原型链的基本构想。
下面举例说明原型链模式~
function parent(){
this.flag = true //该方法定义一个属性
}
parent.prototype.getParentValue = function(){
return this.flag
}
console.log(parent.prototype)
function child(){
this.flag1 = false
} //创建一个构造函数child
child.prototype = new parent() //让child通过创建实例,继承parent
console.log(child.prototype) //child的原型就是parent函数
child.prototype.getChildValue = function(){
return this.flag1
}
let instance = new child()
console.log(instance.getParentValue()) //true
以上三处打印值分别如下:
将以上的结果对应图再看一下:
总的思想就是:
我们在读取实例上的属性时,首先会在这个实例上搜索到这个属性,如果没找到,则会继承搜索实例的原型 ,在通过原型链实现继承之后,会继续搜索原型的原型。就例子而言,调用instace.getParentValuve()去寻找父节点的函数值的时候,经过了3步搜索,先搜索instace、接着是child.prototype,然后是parent.prototype,最后一步才找到该方法。。。那么对于属性和方法的搜索会一直持续到原型链的末端。
原型链继承的问题
抛出问题1:原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会在原型上定义的原因---重点
用例子来阐述问题
// 继承的问题
function parent(){
this.colors = ['red','blue','white']
}
function child(){}
child.prototype = new parent()
let instance1 = new child()
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "white", "black"]
let instance2 = new child()
console.log(instance2.colors) //["red", "blue", "white", "black"]
看到了吗,解读一下:这个例子中,parent构造函数定义了一个colors属性,其中包好一个数组(引用值),然后每个parent的实例都会有自己的colors属性,并包含着自己的数组,但当child通过原型继承了parent之后,child原型变成了parent的一个实例,因此也获得了自己的colors属性,最终结果就是child所有实例都会共享这个属性,因此instance1的修改会影响instance2的修改
抛出问题2:原型链的第二个问题是子类型在实例化时不能给父类型的构造函数传参,因此原型链方法一般不会单独使用
盗用构造函数继承
构造函数继承是为了解决原型包含引用值会共享而出现的,它的基本思路是:在子类构造函数中调用父类构造函数,可以使用apply()和call()方法(关于apply()和call()方法的使用 可以查看 https://blog.csdn.net/qq_41579104/article/details/108489259) 以新创建的对象为上下文执行构造函数
举个栗子吧~
// 构造函数继承
function parent(){
this.colors = ['red','blue','white']
}
function child(){
parent.call(this) //继承parent
}
let instance1 = new child()
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "white", "black"]
let instance2 = new child()
console.log(instance2.colors) //["red", "blue", "white"]
例子中,通过使用call()或apply()方法,parent构造函数在为child的实例创建的新对象的上下文中执行了,这就相当于新的child对象上运行了parent函数中所有初始化代码 ,结果就是每个实例都会有自己的colors属性
还有一个优点,就是可以在子类构造函数中向父类构造函数传参了,解决了原型链的缺点。
// 传参
function parent(name){
this.name = name
}
function child(){
parent.call(this,'hengheng')//继承parent并传参
this.age = 20 //实例属性
}
let instance = new child()
console.log(instance.name)
console.log(instance.age)
例子解读:这个例子中,parent构造函数接收一个参数那么,然后他赋值给一个属性,在child构造函数中调用parent时传入这个参数,实际上会在child的实例上定义那么属性。
构造函数继承的问题
抛出问题:主要缺点就是必须在构造函数中定义方法,因此函数不能复用,也就是说子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式,因此他也不可以单独使用
组合继承(组合原型链继承和盗用构造函数继承)
组合继承呢,综合了原型链和构造函数,将两者的优点集中了起来,基本思路是:使用原型链继承原型上的属性和方法,通过构造函数继承实例属性,这样既可以把方法定义在原型上实现复用,又可以让每个实例都有自己的属性
一个例子说明一切:
// 组合继承
function parent(name){
this.name = name
this.colors = ["red", "blue", "white"]
}
parent.prototype.sayName = function(){
console.log(this.name)
}
function child(name,age){
// 继承属性
parent.call(this,name)
this.age = age
}
//继承方法
child.prototype = new parent()
child.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new child('hengheng',20)
instance1.colors.push("black")
console.log(instance1)
instance1.sayAge()
instance1.sayName()
let instance2 = new child('suosuo',22)
instance2.colors.push("yellow")
console.log(instance2)
instance2.sayAge()
instance2.sayName()
例子解读:
在这个例子中,parent构造函数定义了两个属性,name和colors,而他的原型上也定义了一个方法是sayname,child构造函数调用了parent构造函数(构造函数继承),传入了name参数,然后又定义了自己的属性age,此外child.prototype也被赋值为parent的实例,原型赋值后,又在这个原型上添加了新方法sayage,这样,就创建了两个child实例,让这两个实例都有自己的属性,包括colors,同时还共享相同的方法,完美的弥补了原型链和构造函数继承的不足。
重点:结合了两种模式的优点,传参和复用
特点:1、可以继承父类原型上的属性,可以传参,可复用。
2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
原型式继承
这个方式主要是介绍了一种不涉及严格意义上构造函数的继承方法,它的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享
// 原型式继承
function object(o){
function F(){} //这个object()函数会创建一个临时构造函数
F.prototype = o //将传入的对象赋值给这个构造函数的原型,
return new F() //然后返回这个临时类型的一个实例,object()是对传入的对象执行了一次浅复制
}
let person = {
name:"hengheng",
friends:[
"xiaobai",
"xiaohuang",
"xiaoli"
]
}
let otherPerson = object(person)
otherPerson.name = "suosuo";
otherPerson.friends.push("xiaozhang")
let anotherPerson = object(person)
anotherPerson.name = "linda";
anotherPerson.friends.push("xiaowang")
console.log(person.friends)//["xiaobai", "xiaohuang", "xiaoli", "xiaozhang", "xiaowang"]
console.log(otherPerson)
console.log(anotherPerson)
原型式继承适用于:你有一个对象,想在它的基础上再创建一个新对象,你需要先把这个对象传给object,然后再对返回的对象进行操作。
例子解读:
这个例子中,person对象定义了另一个对象也应该共享的信息,然后把它传给object()之后会返回一个新对象,这个新对象的原型是person,意味着它的原型上既有原始属性又有引用值属性,这也意味着person.friends不仅是person的属性,也会跟着otherPerson和anotherPerson共享,这里实际上克隆了两个person。
那么在ES5中新增的create()方法将原型式继承的概念规范化了,这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选),在只有一个参数时,Object.create()与这里的objec的作用相同。
举个栗子(Object.create()只有一个参数时):
let person = {
name:"hengheng",
friends:[
"xiaobai",
"xiaohuang",
"xiaoli"
]
}
let anotherPerson = Object.create(person)
anotherPerson.name = "suosuo"
anotherPerson.friends.push("xiaowang")
let otherPerson = Object.create(person)
otherPerson.name = "hengheng"
otherPerson.friends.push("xiaozhang")
console.log(person.friends)
举个栗子(Object.create()有两个参数时):
let person = {
name:"hengheng",
friends:[
"xiaobai",
"xiaohuang",
"xiaoli"
]
}
let anotherPerson = Object.create(person,{
name:{
value:"suosuo"
}
})
console.log(anotherPerson.name)//suosuo
Object.create()的第二个参数作用:以这种方式添加的属性会遮蔽原型对象上的同名属性。
原型链继承的问题:类似于复制一个对象,用函数来包装(特点),它非常适合不需要创建构造函数,但仍需要在对象间共享信息的场合(优点),但是,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式一样,无法实现复用(缺点)。
寄生式继承
该继承方式与原型式继承比较接近,它的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
// 寄生式继承
function object(o){
function F(){} //这个object()函数会创建一个临时构造函数
F.prototype = o //将传入的对象赋值给这个构造函数的原型,
return new F() //然后返回这个临时类型的一个实例,object()是对传入的对象执行了一次浅复制
}
function createAnother(o){
let clone = object(o) //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式增强对象
console.log("hi")
}
return clone; //返回这个对象
}
let person = {
name:"hengheng",
friends:[
"xiaobai",
"xiaohuang",
"xiaoli"
]
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() //打印出 hi
console.log(anotherPerson)
这个例子的重点强调是:基于person对象新返回了一个新对象,新返回的anotherPerson对象具有person的所有属性和方法,还有一个新方法是sayHi
寄生式继承的问题:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
组合继承其实存在效率问题,最主要的效率问题是父类构造函数始终被调用两次,一次是在创建子类原型时调用,领一次是在子类构造函数中调用,本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行的时重写自己的原型就行了。
基本思想:寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
为了解决上面的问题,所以需要在混合继承的基础上进行改造。那么如何避免冗余呢?
- 避免使用
child.prototype = new parent()
来继承整个parent
实例; - 这样将代码改写为
child.prototype = parent.prototype
; - 那么又会引入一个问题,child.prototype与parent.prototype共用一个空间(在正常继承中,child.prototype应该有自己独立的空间),也就是说一旦我们修改了child.prototype,同时也修改了parent.prototype;
- 所以继续修改代码为
child.prototype=Object.create(parent.prototype)(
在支持ES5的浏览器中可以直接使用),其实这个create的作用跟咱们下面的object()函数的功能是一样的
- 函数中返回的F即为child.prototype的独立空间
举个栗子:
// 寄生组合
function object(o){
function F(){} //这个object()函数会创建一个临时构造函数
F.prototype = o //将传入的对象赋值给这个构造函数的原型,
return new F() //然后返回这个临时类型的一个实例,object()是对传入的对象执行了一次浅复制
}
function inheritFunc(child,parent){
let prototype = object(parent.prototype) //创建父类原型的一个副本,也可以写为let prototype = Object.create(parent.prototype)
prototype.constructor = child //增强对象 给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题
child.prototype = prototype //赋值对象 将新创建的对象赋值给子类型的原型
}
这个inheritFunc()函数实现了寄生式组合继承的核心,这个函数接收两个参数:子类构造函数和父类构造函数,在这个函数内部,注意:
第一步 是创建父类原型的一个副本,然后给返回的prototype对象设置constructor属性
第二步 是解决由于重写原型导致默认constructor丢失的问题,constructor的指向问题(改不改都不影响),原来返回的F.constructor指向parent,要修改为指向child
最后将新创建的对象赋值给子类型的原型,那么接下来,调用inheritFunc()就可以实现前面例子中的子类型原型赋值:
function parent(type){
this.type = type
this.children = ['c1','c2']
}
parent.prototype.say = function(){
console.log(this.type+" "+this.name+":这是我的孩子们"+this.children)
}
function child(type,name){
parent.call(this,type)//借用构造函数 继承属性
this.name = name
}
inheritFunc(child,parent)//原型链,用于继承parent.prototype(say方法)上的方法
var p = new child("worker","suoh")
p.say() //worker suoh:这是我的孩子们c1,c2
console.log(p)
var v = new child("student","wweiq")
v.children.push("禽兽")
v.say() //student wweiq:这是我的孩子们c1,c2,禽兽
p.say() //worker suoh:这是我的孩子们c1,c2
console.log(v)
这里只调用了一次parent构造函数,避免了child.prototype上不必要也用不到的属性,因此可以说这个例子的效率更高,而且原型链仍保持不变,因此可以说寄生式组合继承是引用类型继承的最佳模式。
总结
JS的继承主要通过原型链来实现,原型链涉及把构造函数的原型赋值为另一个类型的实例。这样一来,子类就可以访问父类的所有属性和方法,就像基于类的继承那样。
- 原型链 的问题是所有继承的树形和方法都会在对象视力健共享,无法做到实例私有。
- 盗用构造函数 模式通过在子类构造函数中调用父类构造函数,可以避免这个问题。这样可以让每个实例竭诚的树形都是私有的,但要求类型只能通过构造函数模式来定义(因为子类不能访问父类原型上的方法)。
- 组合继承 最为流行,即通过原型链继承共享的属性和方法,通过盗用构造函数来继承实例属性。
- 原型式继承 可以无需明确定义构造函数二实现继承,本质上是对给定对象执行浅复制,这个操作的结构之后还可以再进一步增强。
- 寄生式继承 与原型式继承密切相关,即先基于一个对象创建一个新对象,然后再增强这个新对象,最后返回新对象,这个模式也被用在组合继承中,用于避免重复调用父类构造函数导致的浪费。
- 寄生组合继承 被认为是实现基于类型继承的最有效的方式。
最后想说ES6新增的类很大程度是基于既有原型极致的语法糖,类的语法让开发者可以优雅的定义向后兼容的类,既可以继承内置类型,也可以继承自定义类型。类有效地跨越了对象实例、对象原型和对象类之间的鸿沟,,,那么关于类的继承是怎么一回事呢,待后续更新讲解。