面试失利后连夜翻开了红宝书,原来继承这么简单
前言
面试官:“把你知道的继承方法都说下吧”。 我竟然一时语塞。。。。。我明明都背过啊😅,怎么又忘了,看来还是得下来理解理解(背得不够熟)😤,于是我连夜把那本放在书堆最底下的外号叫做红宝书的《Javascript高级程序设计》给翻了出来。
原型链
原型和实例的关系
原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针
只能说不愧是经典书籍,一句话就给说清楚了。这里实例包含一个指向原型对象的内部指针事实上就是[[Prototype]]
,可以通过object.__proto__获取到,我们来打印看看
let obj = {}
console.log(obj,obj.__proto__);
然后我们把构造函数的原型对象指向另一个构造函数的实例,这个实例同样包含一个指向原型对象的内部指针[[Prototype]]
,我们再把这个原型对象指向另另一个构造函数的实例,这个实例也同样包含指向它的原型对象的内部指针,如此层层递进,就构成了实例与原型的链条。
利用原型链实现继承
看下面这段代码
function SuperType(){
this.color = ['red']
}
function SubType(){
this.books = ['红宝书']
}
SubType.prototype = new SuperType
let obj1 = new SubType
let obj2 = new SubType
obj1.books.push('犀牛书')
obj1.color.push('yellow')
console.log(obj2.books,obj2.color); //[ '红宝书' ] [ 'red', 'yellow' ]
可以看到基于原型链实现的继承存在一个问题,就是父类的属性是一个引用类型的时候会被所有子类共享,原因是跟new有关,我们在使用new关键字来创建实例的时候会把this赋给被创建的对象,这里SubType.prototype = new SuperType
就等同于SubType.prototype.color = ['红宝书']
,无论调用obj1.color还是obj2.color都是用的原型上的color属性,这也就不难理解为什么父类的属性会被子类共享了。
那该如何解决这个问题呢?聪明的程序员们想出了借用构造函数的办法👊
借用构造函数
看下这段代码:
function SuperType(name){
this.color = ['red']
this.name = name
}
function SubType(){
SuperType.call(this,'食困症患者')
this.books = ['红宝书']
}
let obj1 = new SubType
let obj2 = new SubType
obj1.books.push('犀牛书')
obj1.color.push('yellow')
console.log(obj2.books,obj2.color); //[ '红宝书' ] [ 'red' ] 食困症患者
这里我们在SubType
这个构造函数内,利用call
把子类的this
指针赋给了父类,也就相当于把父类的属性全部传给了子类,并且还有传递参数。
不过问题又来了,父类原型上的函数我们没有继承到啊。那如果把函数都定义在属性上呢?这并不是一个好办法,因为当我们在使用new关键字的时候,会给每个属性重新分配内存空间,这会导致每次构造一个实例都会重新生成一个新的函数;而理想情况是把函数定义在原型上,原型上的函数只会生成一次,为了解决这个问题我们还得另辟蹊径。
组合继承
思考下面这段代码
function SuperType(name){
this.color = ['red']
this.name = name
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(){
SuperType.call(this,'食困症患者')
this.books = ['红宝书']
}
SubType.prototype = new SuperType
SubType.prototype.constructor = SubType
let obj2 = new SubType
obj2.sayName() //食困症患者
这里通过把SuperType的实例赋值给SubType的原型,形成原型链,使得obj2能够调用父类的函数,并且把父类的属性通过借用构造函数把属性传给了子类的构造函数,这也使得这种继承方式成为了Javascript中最常用的继承模式,不过还是有一点不足我们后面再说😏。
不知道大家有没有注意到这行代码:SubType.prototype.constructor = SubType
,我们在讲原型和实例的关系的时候有提到原型对象包含一个指向构造函数的指针,而SubType.prototype = new SuperType
把这个指针给覆盖掉了,所以我们需要显式地重新指向这个原型对象本身的构造函数。
原型式继承
function object(o){
function func(){}
func.prototype = o
return new func
}
原型式继承也就是创建一个空的构造函数,把需要被继承的对象传进object函数,并赋值给构造函数的原型对象,最后返回这个构造函数的实例
寄生式继承
function create(o){
let obj = object(o)
obj.name = '食困症患者'
return obj
}
寄生式继承在原有的object基础上进行了一次封装,对返回的实例增强;不过缺点很明显,object函数里的参数还是会共享引用类型,像这样:
function object(o){
function Func(){}
Func.prototype = o
return new Func
}
function create(o){
let obj = object(o)
obj.name = '食困症患者'
return obj
}
let o = {
color:['red'],
}
let obj1 = create(o)
let obj2 = create(o)
obj1.color.push('yellow')
console.log(obj2.color); //[ 'red', 'yellow' ]
寄生组合式继承
前面我们说过组合式继承有一点不足,是因为无论如何组合式继承都会调用两次超类型的构造函数,第一次调用会把父类的属性赋值给子类构造函数的原型上,而这些属性是不会用上的,因为第二次调用父类的构造函数的时候会把相同的属性给子类的构造函数上,所以之前赋值给子类构造函数的原型上的同名属性会被屏蔽掉
function SuperType(){
this.name = '食困症患者'
}
function SubType(){
SuperType.call(this) //第二次调用构造函数
}
SubType.prototype = new SuperType //第一次调用构造函数
SubType.prototype.constructor = SubType
let obj = new SubType
也就是说SubType构造的实例和SubType的原型对象上存在同名的属性name,下面来看下寄生组合式继承如何解决这个问题:
function object(o){
function Func(){}
Func.prototype = o
return new Func
}
function InheritPrototype(SubType,SuperType){
let prototype = object(SuperType.prototype)
prototype.constructor = SubType
SubType.prototype = prototype
}
function SubType(){
SuperType.call(this) //调用父类构造函数
}
function SuperType(){
}
let obj = new SubType
这里只调用了一次父类的构造函数,并且object函数中传递的是父类的原型对象SuperType.prototype
,然后子类的原型对象指向object返回的实例,因为定义在父类的原型上通常定义的是函数,所以也不会存在同名属性被屏蔽掉的问题,可以说是非常巧妙了😆。
总结
重新读了一遍红宝书继承这一部分的知识获益良多,经典之所以是经典,每次翻阅都能学到不一样的东西,它总能用最精简的语言描述清楚其中的逻辑,而我在面试官面前只会啊吧啊吧啊吧😤
转载自:https://juejin.cn/post/7041243935213092900