注册
web

JS实现继承的几种方式


继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。



下面我们就来看看 JavaScript 中都有哪些实现继承的方法。


原型链继承


原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。



原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例



下面我们结合代码来了解一下。



function Animal (name) {

  // 属性

  this.name = name

  this.type = 'Animal'

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}




// 子类

function Cat (name) {

  this.name = name

}

// 原型继承

Cat.prototype = new Animal()

// 将Cat的构造函数指向自身

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

console.log(cat.type) // Animal

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。


在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。


同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。



需要注意:
Cat.prototype.constructor = Cat


如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。



原型链继承的优点


简单,易于实现


只需要设置子类的prototype属性指向父类的实例即可。


可通过子类直接访问父类原型链属性和函数


原型链继承的缺点


子类的所有实例将共享父类的属性


子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享。



function Animal () {

  this.skill = ['eat', 'jump', 'sleep']

}

function Cat () {}

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat1 = new Cat()

let cat2 = new Cat()

cat1.skill.push('walk')

console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]

console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]


在子类实例化时,无法向父类的构造函数传参


在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。


无法实现多继承


子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。


构造函数继承(借助 call)



构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。




  // 父类

function Animal (age) {

  // 属性

  this.name = 'Animal'

  this.age = age

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}

function Cat (name) {

  // 核心,通过call()函数实现Animal的实例的属性和函数的继承

  Animal.call(this)

  this.name = name

}




let cat = new Cat('Tom')

cat.sleep() // Tom正在睡觉

cat.eat() // Uncaught TypeError: cat.eat is not a function


通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数


构造继承的优点


解决了子类实例共享父类属性的问题


call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。


创建子类的实例时,可以向父类传参



   // 父类

function Animal (age) {

  this.name = 'Animal'

  this.age = age

}

function Cat (name, parentAge) {

  // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承

  Animal.call(this, parentAge)

  this.name = name

}




let cat = new Cat('Tom', 10)

console.log(cat.age)


可以实现多继承


在子类的构造函数中,可以多次调用call()函数来继承多个父对象。


构造函数的缺点


实例只是子类的实例,并不是父类的实例


因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。


只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数


与上面原因相同。


无法复用父类的构造函数


因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。


组合继承



组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。




// 父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}




// 子类

function Cat (name) {

  // 通过构造函数继承实例的属性和函数

  Animal.call(this)

  this.name = name

}

// 通过原型继承原型对象上的属性和函数

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


组合继承的优点


既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数


既是子类的实例,又是父类的实例


不存在引用属性共享的问题


构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。


可以向父类的构造函数中传参


组合继承的缺点


父类的实例属性会被绑定两次


在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。


寄生组合继承


组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。


在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数



  //父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}

// 子类

function Cat (name) {

  // 继承父类的实例和属性

  Animal.call(this)

  this.name = name

}

// 继承父类原型上的实例和属性

Cat.prototype = Object.create(Animal.prototype)

Cat.prototype.constructor = Cat

let cat = new Cat('Tom')




console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头



其中最关键的语句:






Cat.prototype = Object.create(Animal.prototype)






只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。



这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。


整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。


总结


image.png


作者:蜡笔小群
来源:juejin.cn/post/7168856064581091364

0 个评论

要回复文章请先登录注册