前端面试题:js的继承

417 阅读6分钟

前言

今天给大家介绍的是面试官经常会问到的一个问题--继承,所谓继承就是让子类的实例能够访问到父类的属性和方法,在js中能够实现继承的方法有很多,以下包含所有方法的总结(总共有七个方法)。

正文

首先来看一串代码:

function Parent(){
    this.name='Tom'
}

function Child(){
    this.type='children'
}

let child=new Child()
console.log(child.name)

运行结果为: image.png 按照上面代码例子,如果我们打印出Tom就算成功继承到了,下面我们通过各种方法来实现。

方法一:原型链继承

function Parent(){
    this.name='Tom'
}

function Child(){
    this.type='children'
}

Child.prototype=new Parent()
let child=new Child()
console.log(child.name)

运行结果为: image.png 可以看到,成功打印出了Tom,上面代码中,我们都知道child实例对象首先会查找自身的属性,如果没有找到JavaScript引擎会沿着原型链向上查找,直到找到name属性为止,那么接下来就会去自己的隐式原型上找,而实例对象的隐式原型child. _proto_ 就是等于构造函数的显示原型Child.prototype,我们通过把new出来的Parent的实例对象赋值给Child.prototype(child的显示原型),那么child.prototype就会具有Parent含有的属性和方法,child就可以向上找,成功找到name属性。

但是这个方法有个问题,当创建多个子类实例时,子类实例继承同一个原型对象,内存是共享的,所有实例之间会相互影响,接下来我看看影响表现出来是什么:

function Parent(){
    this.name='Tom'
    this.like=[1,2,3]
}

function Child(){
    this.type='children'
}
Child.prototype=new Parent()

let s1=new Child()
let s2=new Child()
console.log(s2.like)
s1.like.push(4)
console.log(s2.like)

运行结果:

image.png

我们可以看到实例对象s1修改了Parentlike属性,但是我们都知道子类不是不能修改父类显示原型上的属性吗,但在上述情况下,由于Child.prototype直接指向一个Parent的实例,子类实例确实可以修改从父类继承的实例属性,因为它们实际上是在修改那个具体的Parent实例,不是修改父类的显示原型,所以s1修改的是实例上的属性,而s1s2共用一个创建出来的Parent实例,内存是共享的,导致了s2like属性的变化,所有s2是[1,2,3,4]。

方法二:构造函数继承

Parent.prototype.getName=function(){
    return this.name
}

function Parent(){
    this.name='Tom'
    this.like=[1,2,3]
}

function Child(){
    Parent.call(this)  //把父类的属性都继承过来(显示)
    this.type='children'
}

let s1=new Child()
let s2=new Child()  
s1.like.push(4)
console.log(s2.like)
console.log(s1.getName())

运行结果为:

image.png 我们可以看到s1修改了like属性但是s2打印出来还是[1,2,3],说明该方法的子类实例之间互不影响,其原理在Child里面写Parent.call(this),相当于调用Parent构造函数,但与直接调用new Parent() 不同,它不会创建一个新的Parent实例,并且这里的this指的是当前正在构造的Child实例,这意味着Parent构造函数内部的this.namethis.like将会被添加到Child实例上,而不是创建一个与Child实例分离的Parent实例,所以s1的修改对s2没有影响。但是可以看到我们打印出来的s1.getName() 报错,说明该方法的子类实例无法继承父类原型上的属性或方法,概括为该方法的子类实例之间互不影响,但是不能继承到父类的原型。

方法三:组合继承

Parent.prototype.getName=function(){
    return this.name
}

function Parent(){
    this.name='Tom'
    this.like=[1,2,3]
}

function Child(){
    Parent.call(this)  
    this.type='children'
}
Child.prototype=new Parent()  


let s1=new Child()
let s2=new Child()  
s1.like.push(4)
console.log(s2.like)
console.log(s1.getName())

运行结果为:

image.png 我们通过方法一和方法二的结合也就是方法三很好解决了它们各自的问题,但是这个方法有一个问题,我们看下面:

image.png 按道理s1._proto_ 有一个constructor是指向自己的原型Child,但是通过这个方法s1._proto_ 被弄没了,我们知道Child.prototype是等于s1._proto_ 的,所有我们在代码中加入Child.prototype.constructor=Child,我们人为让s1._proto_ 的 constructor指向Child。修改后我们再看:

image.png 我们成功的把s1._proto_的constructor指向了Child,到目前为止这个方法没什么缺点,但是在这个方法中父类需要被调用执行两次,性能开销大,其实也没那么大了,所以还要接着改进。

方法四:原型式继承

let parent={
    name:'Tom',
    age:40,
    like:[1,2]
}


let  child1=Object.create(parent)
let  child2=Object.create(parent)
child1.like.push(3)
console.log(child2.like)

运行结果为: image.png 我们通过Object.create()parent对象拷贝一个一模一样的对象然后赋给child1,child2,也就实现了对象的继承,但是它只是简单拷贝,多个实例之间继承当是引用类型的时候,他们的地址是相同的,所以实例之间会相互影响。它的名字类似于原型链继承,缺点是类似的,但它是对象之间的继承,那为啥还要创建这一个有缺点的方法,方法三不是很好了吗,那这个方法就是要运用到后面使方法三更完美。

方法五:寄生式继承

let parent={
    name:'Tom',
    age:40,
    like:[1,2]
}

function clone(obj){
    let clone=Object.create(obj)
    clone.getLike=function(){
        return this.like;
    }
    return clone;
}
let child1=clone(parent)
let child2=clone(parent)
console.log(child2.getLike());
child2.like.push(3);
console.log(child2.getLike());

运行结果为: image.png 我们可以看到问题依旧存在,实例之间依旧会有影响,但是它有个优点就是可以让子对象默认具有自己的属性。

方法六:寄生组合式继承

Parent.prototype.getNmae=function(){
    return this.name
}


function Parent(){
    this.name='Tom'
    this.like=[1,2,3]
}

function Child(){
    Parent.call(this)  
    this.type='children'
}
Child.prototype=Object.create(Parent.prototype) 
Child.prototype.constructor=Child

let s1=new Child()
let s2=new Child()  

我们可以看到通过Child.prototype=Object.create(Parent.prototype)Parent的显示原型传给一个新对象然后再给Child,并且子类的修改不会影响分类,这个方法来很好的解决了组合继承父类需要被调用两次的问题,并且还解决了组合继承父类中的属性被显示继承了一遍又被隐式继承了一遍,导致继承两遍浪费的情况。到es6之前算是最好用的方法了。后来js觉得java里面类方法的继承很好用就在es6打造了一个更好用的方法。

方法七:class继承

class Parent{
    constructor(name){
        this.name = name;
    }
    getName(){
        return this.name;
    }
}

class Child extends Parent{
    constructor(name){
        super(name)
        this.age = 20
    }
}

let c=new Child('Jerry');
console.log(c.getName());

运行结果为: image.png 子类通过extends继承父类,要在子类里调用super() 才能实现继承父类里面的所有东西,在constructor() 里面写属性,外面写方法,很容易理解,使用起来非常方便。

总结

今天介绍的七种继承方法就到这里,感谢大家的阅读,希望对你有所帮助!可以帮忙点点赞吗,非常感谢!