Javascript中几种常见的继承方式

Javascript 中继承的主要方式是原型链,即通过原型继承多个引用类型的属性和方法。

首先温习一下原型链。

每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。

假如存在如下父类,希望子类去继承该父类,有哪些可以实现继承的方式?

function Parent() {
  this.age = 10
}
Parent.prototype.name = 'parent name'
Parent.prototype.from = 'parent from'

原型链继承

原型链继承就是将父类实例化赋值给子类的原型。那么基于原型链的原理,子类可以读取父类中的属性和方法。

function Child() {}

Child.prototype = new Parent()
Child.prototype.name = 'child name'

var child = new Child()
console.log(child.name) // child name
console.log(child.from) // parent from

这种方式主要有以下两种问题:

1.当原型中包含引用值的时候会在所有实例之间共享,导致意想不到的问题。比如如下示例。

function Parent() {
  this.friends = ['A', 'B']
}

function Child() {}
Child.prototype = new Parent()

var child1 = new Child()
child1.friends.push('C')
console.log(child1.friends) // [ 'A', 'B', 'C' ]
var child2 = new Child()
console.log(child2.friends) // [ 'A', 'B', 'C' ]

虽然只在child1中进行了push操作,但是这个操作会影响到child2这个新实例。

这是因为,Child的原型为Parent的一个实例对象,那么Parent身上的实例属性变成了Child身上的原型属性,我们知道原型属性是共享的,就意味着Child的所有实例都会共享Parent中的friends属性,所以出现了上述问题。

2.子类在实例化的时候无法给父类构造函数传参。

盗用构造函数继承

为了防止原型对象中的引用值共享问题,采用了盗用构造函数的继承方式。它的实现方式是在子类构造函数中通过callapply将父类中的实例对象属性和方法转换为子类属性和方法,这样就会让每一个子类实例都有自己的方法和属性了。

function Child() {
  // 盗用构造函数
  Parent.call(this)
}
Child.prototype.name = 'child name'
console.log(child.name) // child name
console.log(child.from) // undefined
console.log(child.age) // 10

存在的问题。

1.从上面的例子中我们可以看到,child.from输出的是undefined,所以,第一个问题就是他不能访问父类原型上定义的方法和属性。

2.必须要在构造函数中定义方法,就意味函数不能重用。

实例继承

这种继承方式是直接在子类中去实例化父类构造函数。

function Child() {
  return new Parent()
}
Child.prototype.name = 'child name'
console.log(child.name) // parent name
console.log(child.from) // parent from
console.log(child.age) // 10

它带来的问题:不能多次继承。

组合式继承

组合式继承将原型链继承和盗用构造函数继承两者的优点结合了起来。使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

function Child() {
  Parent.call(this)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
Child.prototype.name = 'child name'
var child = new Child()
console.log(child.name) // child name
console.log(child.from) // parent from
console.log(child.age) // 10

缺点很明显,从上面的示例中也可以看到,Parent其实被实例化了两次,一次是在创建子类原型时调用,另一次是在子类构造函数中调用。

这种继承方式在之前的文章(JavaScript 继承最佳实践)中也有所介绍。

寄生组合继承

上述组合式继承存在调用两次父类Parent的情况,而寄生组合继承则可以避免上述这种情况出现。

主要的不同点在于不通过调用父类构造函数给子类原型赋值,而是获取父类原型的副本,创建一个新的对象,将这个父类原型副本传递给新对象的原型,最后实例化这个新对象。

function Child() {
  Parent.call(this)
}
;(function () {
  var Super = function () {}
  Super.prototype = Parent.prototype
  Child.prototype = new Super()
})()
// 重写导致constroctor丢失,此处修正constroctor
Child.prototype.constructor = Child
Child.prototype.name = 'child name'
var child = new Child()
console.log(child.name) // child name
console.log(child.from) // parent from
console.log(child.age) // 10

关于这种继承方式,之前的文章(JavaScript 继承最佳实践)也有所介绍。

es6 继承

可以说是最清晰明了的一种继承方式,直接使用extends关键字即可。

class Parent {
  name = 'parent name'
  from = 'parent from'

  constructor() {
    this.age = 10
  }
}

class Child extends Parent {
  name = 'child name'
}

const child = new Child()
console.log(child.name) // child name
console.log(child.from) // parent from
console.log(child.age) // 10

使用extends关键字,可以继承任何拥有[[Constructor]]和原型的对象。这意味着,不仅可以继承一个类,而且还可以继承普通的构造函数。

class ParentDemo1 {}

// 继承class类
class Child extends ParentDemo1 {}

const child = new Child()
console.log(child instanceof Child) // true
console.log(child instanceof ParentDemo1) // true

function ParentDemo2() {}

// 继承普通的构造函数
class Child2 extends ParentDemo2 {}

const child2 = new Child2()
console.log(child2 instanceof Child2) // true
console.log(child2 instanceof ParentDemo2) // true

对于 es5 和 es6 继承而言,它们存在一定的区别,可以参见ES6 和 ES5 中的继承关系梳理

总结

本文主要对 js 几种常见的继承方式做一个简要总结,了解不同继承的实现思路和它们之间的异同点,是十分有必要的。

所以,前端漫漫长路,加油吧,骚年!💪🏻

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注