今天的主题就是javascript里面的类以及其对应的继承方式,这在我们平时的面试当中经常会被问到,那么我们就通过这篇文章来讲解一下。
两种类的声明方式
1.以函数的方式声明(ES5的方式)
function Person(name){
this.name = name
}
2.以类的方式声明(ES6的方式)
class Person2{
constructor(name){
this.name = name
}
}
谈谈类的继承,继承有两种方式:
1.通过更改this指向来继承父类的属性。
2.通过原型链来继承父类的属性。
1.更改this指向的方式
/* 先来创建两个类 */
/* 父类 */
function Parent(){
this.name = "张三"
this.age = 34
this.hobby = ["篮球"]
}
/* 子类,本例子子类继承父类 */
function Children(){
/* 解释一下这一句代码,在我们将Children类实例化时,this指针会指向子类的实例对象,也就是在执行父类的构造函数把父类挂载到子类的实例对象之下 */
Parent.call(this)
/* Parent.apply(this)也可以 */
this.age = 14
}
我们来通过打印验证一下
console.log(new Children()) //此时我们在子类的实例化对象之下就能看到父类函数中的name属性,顺利实现继承
/* 缺点:子类无法继承父类的原型链上的属性,我们可以在父类的原型上加个属性观察子类实例是否打印的出来 */
Parent.prototype.gender = "男"
console.log(new Children()) //没有打印出父类的原型对象上的属性,没实现继承
2.通过将子类构造函数的原型对象(既prototype)赋值为父类的实例
function Children2(){
this.name = "李四"
}
/* 依旧沿用上边的父类 */
Children2.prototype = new Parent()
/* 我们都知道,在原型链中,一个构造函数的prototype跟其对应的实例对象的__proto__都指向原型对象 */
/* 即我们所理解的:Children2.prototype = new Children2().__proto__,所以我们打印子类的实例对象结果如下 */
打印如下:
console.log(new Children2())
//所以此时在Children2的原型链中寻找Parent原型链上的属性时就没有问题了,顺利实现继承
那么这个方式同样也会有缺点,原型链上的属性,牵一发而动全身。
/* 此时我们创建两个都为Children2实例化的对象 */
var child1 = new Children2()
var child2 = new Children2()
//都先打印一下我们接下来要测试的属性
console.log(child1.hobby,child2.hobby)
//接下来我们更改其中一个实例的hobby属性值
child1.hobby.push("敲代码")
//再输出两个实例的hobby属性
console.log(child1.hobby,child2.hobby) //会发现,更改实例1后,两个实例的hobby都修改了,在日常开发业务中,两个实例应该是相互隔离互不影响的。
原因,child1的修改是在 child1.__proto__上面的Parent修改的,而child1.proto===child2.proto,故两个实例的同一条原型链上的属性被修改。
那么以上两种方式都有各自的缺点,那尝试一下将两种组合起来。
3.组合继承
function Parent3(){
this.name = "张三"
this.hobby = ["篮球"]
}
function Children3(){
Parent.call(this) //通过修改执行构造函数的上下文
this.age = 14
}
Children3.prototype = new Parent3() //此时就做到构造函数继承,也做到原型链继承
var child3 = new Children3()
var child4 = new Children3()
//接下来修改hobby属性
child3.hobby.push("游泳")
//打印
console.log(child3,child4) //此时你会发现,两个实例构造函数的hobby互不影响,因为在修改对象时,先从构造函数中寻找属性,构造函数中没有则进入到对应的原型链中去往上寻找
打印结果如下:
同样也存在一个缺点:父类构造函数执行两次,实际影响性能 ,那么我们优化一下。
组合继承优化
function Parent4(){
this.name = "张三"
this.hobby = ["篮球"]
}
function Children4(){
Parent.call(this) //通过修改执行构造函数的上下文
this.age = 14
}
/* 为了不让父类构造函数执行两次,我们通过让父类构造函数和子类构造函数指向同一个原型对象 */
Children4.prototype = Parent4.prototype
var child5 = new Children4()
console.log(child5 instanceof Children4,child5 instanceof Parent4) //true true
/* 此时输出两个true 我们再往实例的constructor 里面寻找 */
console.log(child5.constructor)
//会发现,输出的是Parent4构造函数,就是因为children4和parent4的原型指向同一个对象,所以现在实例是通过父类还是子类进行实例化这个问题一下子模糊了。
打印如下:
组合继承再优化(完美方案)
我们上面谈到,如果将子类和父类的原型指向同一个原型对象,会造成实例的原型指向不清的问题,那么我们就要思考,是不是应该把这两个类的原型对象区分开来,不要混淆一谈,再来进行继承会更好。
重点:Object.create(父类原型对象)
function Parent5(){
this.name = "张三"
this.hobby = ["篮球"]
}
function Children5(){
Parent.call(this) //通过修改执行构造函数的上下文
this.age = 14
}
//通过Object.create()去创建一个新的对象,Object.create创建的对象是通过原型去继承的,那么让Object.create(父类原型)就可以让Children5和Parent5间接的联系起来而又不指向同一个原型对象。
Children5.prototype = Object.create(Parent5.prototype)
//此时你会发现再去寻找children5的原型链时,child6的prototype中间会有一个object与Parent5分隔出来。但如果没将子类构造函数的原型链上的constructor重新定向,我们在寻找实例的constructor时,刚刚Object,create创建的对象依旧还是会指向Parent5,所以只有这一句时,child6实例从哪个类的构造函数出来的依旧指向不清。
//此时我们再将children5原型链里的构造器重新指向回Children5。
Children5.prototype.constructor = Children5
var child6 = new Children5()
console.log(child6 instanceof Children5,child6 instanceof Parent5) //true true
/* 此时输出两个true 我们再往实例的constructor 里面寻找 */
console.log(child6.constructor) // 此时就会明确指向到children5了
打印结果如下:
总结:根据上面的几种方案,并进行不断地优化,我们得出继承地完美方案地关键两句代码如下:
Children5.prototype = Object.create(Parent5.prototype)
Children5.prototype.constructor = Children5
看完这篇文章的小伙伴有没有get到新的技能点呢,如果有可以点个赞支持一下作者,如果认为文章有可以改进地地方,可以在评论区提出您宝贵的建议,谢谢!