关于JavaScript继承的几种方法

前言

继承是JavaScript面向对象编程非常重要的一个特性,基本在日常使用以及面试过程中都会使用到。查阅一些资料后整理以下几种继承的方法记录在自己的博客中。

如何实现继承

原型链继承

原型链继承是最简单的一种继承方式,只需要将子类的prototype值等于父类的实例即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal(){
this.superType = 'Animal'
}
Animal.prototype.getSuperType = function(){
console.log(this.superType);
}
function Cat(name){
this.name = name;
this.type = 'Cat';
}
// 原型继承
Cat.prototype = new Animal();

var cat = new Cat()

cat.getSuperType(); // 控制台输出Animal

以上代码是将Animal的实例覆盖Cat的原型,本质是重写原型对象,代之一个新类型的实例。使Cat拥有Animal实例的所有属性和方法(getSuperType为原型方法),并且还有个指针指向了Animal的原型。当创建Cat的实例cat时,cat指向的是Cat的原型,Cat的原型又指向Animal的原型。

缺点

  • 引用类型值的原型属性会被共享
  • 在创建子类型的实例时,无法向超类型的构造函数传递参数

构造函数继承

借用构造函数继承也是非常简单地一种继承方式,即在子类构造函数的内部调用父类型构造函数。代码实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Animal(name){
this.name = name;
}
Animal.prototype.getName = function(){
console.log(this.name);
}
function Cat(){
Animal.call(this, "cat");
this.eat = "fish"
}

var instance = new Cat();
console.log(instance.name);
console.log(instance.eat); // 控制台相继输出 cat fish
instance.getName() // error getName undefined

以上代码中的 Animal 只接受一个参数 name ,该参数会直接赋给一个属性。在 Cat 构造函数内部调用 Animal 构造函数时,实际上视为 Cat 的实例设置了 name 属性。为了确保 Animal 构造函数不会重写子类的属性,可以在调用父类构造函数后,再添加应该在子类型中定义的属性。

优点

  • 引用类型的原型属性不会被共享
  • 可以在子类型构造函数中向父类构造函数传递参数

缺点

  • 方法都在构造函数中定义,函数无法复用
  • 在父类的原型定义的方法,对子类型是不可见,导致所有的类型都只能使用构造函数

组合继承

组合继承,也叫做伪经典继承,指的是将原型链和借用构造函数组合到一块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function SuperType(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.getName = function() {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name)
this.age = age
}
// 继承方法
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
console.log(this.age)
}
let instance1 = new SubType("tom", "8")
instance1.colors.push("black")
console.log(instance1.colors) // red, blue, green, black
instance1.getName() // tom
instance1.sayAge() // 8

let instance2 = new SubType("jerry", "9")
console.log(instance2.colors) // red, blue, green
instance2.getName() // jerry
instance2.sayAge() // 9

此例子中, SuperType 构造函数定义了两个属性: name colors SuperType 定义了一个方法 sayName() SubType 构造函数在调用 SuperType 构造函数时传入 name 参数,紧接着又定义了它自己的属性 age 。然后将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge 。这样就可以让两个不同的 SubType 实例即拥有自己的属性,又可以使用相同的方法。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceofisPortotypeOf() 也能够用于识别组合继承创建的对象。

缺点

  • 会调用两次超类构造函数,并且会分别在实例和原型上有重复的属性

原型式继承

原型式继承:其思想是借助原型,可以基于已有的对象创建新的对象,同时还不用创建自定义类。以下代码,在 object() 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数原型,最后返回了这个临时类型的一个新实例。从本质上讲, object() 对传入其中的对象执行了一次浅复制。

1
2
3
4
5
function object(o) {
function F(){}
F.prototype = o
return new F()
}
1
2
3
4
5
6
7
8
9
10
11
12
var person = {
name: 'Tom',
friends: ['Jerry', 'Sherry', 'Harry']
}
var anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

var yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Bob')
console.log(person.friends) // Jerry, Sherry, Harry, Rob, Bob

还可以使用 ES5 新增的 Object.create() 方法进行创建

1
2
3
4
5
6
7
8
var anotherPerson = Object.create(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

var yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Bob')
console.log(person.friends) // Jerry, Sherry, Harry, Rob, Bob

如果只是想让一个对象与另一个对象保持类似的情况下,原型式继承是可以完全胜任的

寄生式继承

寄生式继承的思路与构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象

1
2
3
4
5
6
7
function createAnother(original) {
var clone = Object.create(original)
clone.sayHi = function () {
console.log('Hi')
}
return clone
}

上述代码中, createAnother() 函数接收一个参数,也就是将要作为新对象基础的对象。然后,把 original 传递给 object 函数,将返回的结果赋值给clone。再为 clone 对象添加一个新方法 sayHi() ,最后返回 clone 对象。

1
2
3
4
5
6
var person = {
name: 'Tom',
friends: ['Jerry', 'Sherry', 'Harry']
}
var anotherPerson = createAnother(person)
anotherPerson.sayHi() // Hi

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

寄生组合式继承

寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为指定子类行的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype)
prototype.constructor = subType
subType.prototype = prototype
}
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
}

inheritPrototype(subType, SuperType)

SubType.prototype.sayAge = function () {
console.log(this.age)
}

该代码只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上创建不必要的,多余的属性。与此同时,原型链还能保持不变。属于最理想的继承范式。

总结

继承是 JS 中极为重要的一块知识,目前只能参考资料将这些记录下来,完全吃透还需要慢慢实战。