JS 实现继承的几种方式

本文详细介绍了JavaScript中的几种继承实现方式,包括原型链继承、构造函数继承和组合继承,以及它们各自的优缺点。原型链继承简单但存在属性共享问题;构造函数继承解决了共享问题,但无法继承原型函数;组合继承结合两者优点,但也存在属性重复绑定的缺点。最后,文章提出了寄生组合继承作为优化方案,有效避免了上述问题。
摘要由CSDN通过智能技术生成

继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。
下面我们就来看看 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的实例属性,从而避免了父类的实例属性绑定两次。

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

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

总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值