前端面试必备:深入理解JavaScript中的继承

170 阅读5分钟

  在JavaScript中,继承是一种强大的机制,它允许子类继承父类的属性和方法。本文将介绍几种不同的继承方式,包括它们的优缺点以及适用场景。

1. 原型链继承

  原型链继承是最基础的继承方式,它通过设置子类的prototype属性为父类的一个实例来实现继承。这种方式使得所有子类实例都可以访问到父类实例的属性和方法。

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()

s1.like.push(4)

console.log(s1);//Parent { type: 'children' }
console.log(s1.name);//tom
console.log(s1.like);//[1,2,3,4]
console.log(s2.like);//[1,2,3,4]
  • 这段代码中Child.prototype = new Parent()将 Child 构造函数的 prototype 属性设置为一个新的 Parent 实例,那么所有 Child 实例现在都将从 Parent 原型继承属性和方法。
  • 缺点:子类实例会继承同一个原型对象,内存共享,所以实例之间会互相影响,如上述代码中实例s2.like会受s1.like.push(4)的影响导致输出[1,2,3,4]

2. 构造函数继承

构造函数继承通过在子类构造函数中调用父类构造函数来实现,通常使用call()apply()方法。这种方法避免了原型链继承的缺陷,但子类无法访问父类原型上的方法。

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(s1.like);//[1,2,3,4]
console.log(s2.like);//[1,2,3]
console.log(s1.getName());//undefined

通过在Child中调用Parent.call(this),此时同样有s1.like.push(4),但console.log(s2.like)输出的则是[1,2,3],没有受到影响,但s1.getName输出的则是undefined,因为它无法访问父类原型的方法。

3. 组合继承

组合继承结合了原型链继承和构造函数继承的优点,允许子类实例访问父类实例属性和方法,同时保留各自独立的引用类型属性。

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()
Child.prototype.constructor=Child

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

s1.like.push(4)

console.log(s1.like);//[1,2,3,4]
console.log(s2.like);//[1,2,3]
console.log(s1.getName());//tom

但组合式继承的缺点是父类需要执行两次,性能开销相对大一些。

4. 原型式继承

原型式继承利用Object.create()方法创建一个新对象,其原型指向给定的对象。这种方法可以实现继承,但所有实例共享同一原型链上的引用类型属性。

let parent={
    name:'tom',
    age:40,
    like:[1,2],
    getLike:function(){
        return this.like
    }
}
let child =Object.create(parent)
let child2=Object.create(parent)

child.like.push(3)

console.log(child.getLike());//[1,2,3]
console.log(child2.like);//[1,2,3]

但它的缺点也很明显,即多个实例之间继承到的引用类型是相同的地址,会相互影响。

5. 寄生式继承

同原型式继承,但是可以让子对象默认具有自己的属性,但也具有原型式继承同样的缺点,即个实例之间继承到的引用类型是相同的地址,会相互影响。

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=Parent.prototype
Child.prototype=Object.create(Parent.prototype)
Child.prototype.constructor=Child

let s1= new Child()
console.log(s1);//Child { name: 'tom', like: [ 1, 2, 3 ], type: 'children' }
console.log(s1.getName());//tom

由于Child.prototype是通过Object.create(Parent.prototype)设置的,这使得Child的实例能够通过原型链访问Parent原型上的方法。之后设置的Child.prototype.constructor为Child,这是为了修正构造函数的指向,因为Object.create()操作默认会将constructor属性设置为Parent。因此,s1实例将能够访问getName方法。此外,s1将拥有自己的nameliketype属性副本,而不是与其它Child实例共享这些属性。

6. 寄生组合继承

寄生组合继承是ES5中较为优雅的继承方式,它结合了构造函数继承和原型链继承,通过Object.create()方法来创建子类的原型,从而避免了父类构造函数的重复执行。

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=Parent.prototype
Child.prototype=Object.create(Parent.prototype)
Child.prototype.constructor=Child

let s1= new Child()
console.log(s1);
console.log(s1.getName())//tom

7. ES6的类继承

ES6引入了更传统的类语法,通过classextends关键字来实现继承,简化了继承的实现。

class Parent{
    constructor(name){
        this.name=name
        this.like=[1,2]
    }

    getName(){
        return this.name
    }
}
class Child extends Parent{
    constructor(name){
        super(name)//extends+super 继承
        this.age=20
    }
}
let  p =new Child('tom')
console.log(p.getName());//tom

在这段代码中:

  • class Parent定义一个Parent类,包含一个构造函数和一个getName方法。构造函数用于初始化name属性,getName方法用于返回name的值。
  • class Child extends Parent声明Child类继承自Parent类。Child类也有一个构造函数,它通过调用super(name)来调用Parent类的构造函数,确保name属性被初始化。
  • this.age = 20是在Child类的构造函数中额外添加的属性
  • let p = new Child('tom')创建一个Child类的实例p,并传入'tom'作为参数初始化name属性。

总结

  以上几种继承方式中,每种继承方式都有其特定的应用场景,选择合适的方式取决于具体的需求和上下文环境,其中ES6的类继承方式是现代JavaScript中推荐的继承模式,它简洁明了,符合类和对象的传统概念,同时也支持多重继承的特性(通过继承多个基类的子类)。使用class和extends关键字可以让你以更接近传统面向对象语言的方式编写JavaScript代码。希望这篇文章能帮助你在面试和日常开发中更好地理解和应用JavaScript的继承机制。

结尾

  最后,码字不易,如果觉得文章还不错的话,点赞、收藏、关注,三者任选其三即可!感谢~