JavaScript - 继承的几种实现方式

原型链

在 ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。可以总结为:当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype

javascript
1234567891011121314
function Father() {}
Father.prototype.draw = function () {
console.log('Father Draw')
}
Father.prototype.clear = function () {
console.log('Father Clear')
}
// 子类
function Son() {}
Son.prototype.draw = function () {
console.log('Son Draw')
}
缺点:
  • 包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱。
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

基于以上问题,在实践中很少会单独使用原型链。

借用构造函数

借用构造函数的思想主要是在子类型的构造函数中调用超类型构造函数。如下面的例子所示

javascript
12345678910111213141516
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
//继承了 SuperType
SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) //"red,blue,green,black"
var instance2 = new SubType()
console.log(instance2.colors) //"red,blue,green"
优点:
  • 可以在子类型构造函数中向超类型构造函数添加参数
缺点:
  • 和构造函数模式一样的问题,所有的方法都在构造函数中定义,因此就无法做到函数的复用。而且超类型的原型中定义的方法,对于子类型而言也是不可见的。

基于以上问题,借用构造函数的技术也是很少单独使用的。

组合继承

组合继承指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。这种方法的主要思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。如下面的例子所示:

javascript
123456789101112131415161718192021222324252627282930313233
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
//继承属性
SuperType.call(this, name)
this.age = age
}
//继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new SubType('james', 9)
instance1.colors.push('black')
console.log(instance1.colors) //"red,blue,green,black"
instance1.sayName() // "james"
instance1.sayAge() // 9
var instance2 = new SubType('kobe', 10)
console.log(instance2.colors) //"red,blue,green"
instance2.sayName() // "kobe"
instance2.sayAge() // 10
优点:
  • 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPropertyOf() 也能够用于识别基于组合继承创建的对象。
缺点:
  • 调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。

原型式继承

原型式继承的主要思路是可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型,最后返回了这个临时类型的新实例。 从本质上讲,object() 对传入其中的对象执行了一次浅复制。如下面的例子所示

javascript
1234567891011121314151617181920
function object(o) {
function F() {}
F.prototype = o
return new F()
}
var person = {
name: 'Alvin',
friends: ['Yannis', 'Ylu'],
}
var p1 = object(person)
p1.name = 'Bob'
p1.friends.push('Lucy')
var p2 = object(person)
p2.name = 'Lilei'
p2.friends.push('Hanmeimei')
person.friends //Yannis, Ylu, Lucy, Hanmeimei

ECMAScript 5 中新增了 Object.create() 方法规范了原型式继承。这个方法接收两个参数,一个是将被用作新对象原型的对象,一个是为新对象定义额外属性的对象(可选)。

javascript
1234567891011121314
var person = {
name: 'Alvin',
friends: ['Yannis', 'Ylu'],
}
var p1 = Object.create(person)
p1.name = 'Bob'
p1.friends.push('Lucy')
var p2 = Object.create(person)
p2.name = 'Lilei'
p2.friends.push('Hanmeimei')
console.log(person.friends) //Yannis, Ylu, Lucy, Hanmeimei

简单来说这个函数的作用就是,传入一个对象,返回一个原型对象为该对象的新对象。

优点:
  • 可以实现基于一个对象的简单继承,不必创建构造函数
缺点:
  • 与原型链中提到的缺点相同,一个是传参的问题,一个是属性共享的问题。

寄生式继承

寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。如下面的例子所示

javascript
123456789101112131415161718
function createAnother(original) {
var clone = object(original) //通过调用函数创建一个新对象
clone.sayHi = function () {
// 某种方式增强这个对象
console.log('hi')
}
return clone // 返回这个对象
}
var person = {
name: 'james',
}
var anotherPerson = createAnother(person)
anotherPerson.sayHi() // "hi"
优点:
  • 在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。
缺点:
  • 使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。

寄生式组合继承

前面我们提到过了组合继承的缺点,由于调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。

寄生式组合继承就是用来解决这个问题,它与组合继承不同的地方主要是,在继承原型时,我们继承的不是超类的实例对象,而是原型对象是超类原型对象的一个实例对象,这样就解决了基类的原型对象中增添了不必要的超类的实例对象中的所有属性 d 的问题。

我们可以封装继承原型时函数如下:

javascript
1234567
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype) // 创建原型对象是超类原型对象的一个实例对象
prototype.constructor = subType // 弥补因为重写原型而失去的默认的 constructor 属性。
subType.prototype = prototype // 实现原型继承
}
优点:
  • 效率高,避免了在 SubType.prototype 上创建不必要的属性。与此同时还能保持原型链不变,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。