我们今天就来了解下 JS 中的继承,在此之前建议学习JS中的原型和原型链的相关知识。

一种面向对象语言需要向开发者提供四种基本能力:

封装 - 把相关的信息(无论数据或方法)存储在对象中的能力
聚集 - 把一个对象存储在另一个对象内的能力
继承 - 由另一个类(或多个类)得来类的属性和方法的能力
多态 - 编写能以多种方法运行的函数或方法的能力

ECMAScript 支持这些要求,因此可被是看做面向对象的。
目前来说在 JS 中有 7 中实现方式,让我们一起来学习吧。

一、原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ClassA() {
}

ClassA.prototype.color = "blue"
ClassA.prototype.sayColor = function () {
alert(this.color);
}

function ClassB() {
}

ClassB.prototype = new ClassA()
ClassB.prototype.constructor = ClassB
// 重写prototype会丢失constructor属性

优点:1、实例可继承构造函数的属性,父类构造函数属性,父类原型的属性

缺点:1、新实例无法向父类构造函数传参
   2、原型上的属性共享,一个实例修改了原型属性,另一个实例的原型属性也会被修改


二、借用构造函数继承

在子类型的构造函数中调用超类型构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
function ClassA(age) {
this.age = age
}

function ClassB(age,name) {
this.name = name
ClassA.call(this,age)
}

let ClassC = new ClassB(18,'william')

console.log(ClassC) // ClassB { name: 'william', age: 18 }

优点:1、可以向父类构造函数传参
   2、解决了原型中包含引用类型值被所有实例共享的问题

缺点:1、只能继承父类构造函数的属性
   2、函数无法复用(每次都要调用,每个新实例都有父类构造函数的副本)


三、组合继承

组合继承指的是将原型链继承和借用构造函数继承组合到一块,从而发挥二者之长的一种继承模式,比较常用。

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
function ClassA (age) {
this.age = age
this.hobby = 'Run'
}

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

function ClassB (age, name) {
this.name = name
ClassA.call(this, age)
}

ClassB.prototype = new ClassA()
ClassB.prototype.constructor = ClassB
ClassB.prototype.sayName = function () {
console.log(this.name)
}

let ClassC = new ClassB(18, 'william')
console.log(ClassC) // ClassB { name: 'william', age: 18, hobby: 'Run' }

ClassC.sayName() // william
ClassC.sayAge() // 18

特点:1、可以继承父类原型上的属性,可以传参,可复用。
   2、每个新实例引入的构造函数属性是私有的。

缺点:1、调用了两次父类构造函数,子类的构造函数会代替原型上的那个父类构造函数。


四、原型式继承

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。

1
2
3
4
5
function ClassObj(obj) {
function F (){}
F.prototype = obj
return new F()
}

ECMAScript5通过新增 Object.create()方法规范了原型式继承。
在传入一个参数的情况下,Object.create() 和 object() 方法的行为相同。

缺点:1、所有实例都会继承原型上的属性。
   2、无法实现复用。


五、寄生式继承

在原型式继承的基础上包装一个仅用于封装继承过程的函数,即该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function ClassObj (obj) {
function F () { }
F.prototype = obj
return new F()
}

function ExtendObj (obj) {
let newObj = ClassObj(obj)
newObj.sayHi = function () {
console.log(this.name)
}
return newObj
}

let person = {
name: 'William',
age: 18
}

let newPerson = ExtendObj(person)
newPerson.sayHi() //William

优点:1、没有创建自定义类型,因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象。
缺点:1、使用寄生式继承来为对象添加函数,没用到原型,无法复用。
   2、同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。


六、寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 原型式继承
function ClassObj (obj) {
function F () { }
F.prototype = obj
return new F()
}

function ClassPrototype (subType, superType) {
let prorotype = ClassObj(superType.prototype)
prorotype.constructor = subType
subType.prorotype = prorotype
}

function ClassA (name, age) {
this.name = name
this.age = age
}

function ClassB (name, age, height) {
ClassA.call(this, name, age)
this.height = height
}
// ...

优点:只调用了一次超类构造函数,避免在SuberType.prototype上面创建多余的属性,与其同时,原型链还能保持不变。

寄生组合继承是引用类型最理性的继承范式。


七、class…extends

S6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法

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
29
30
31
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//定义一般的方法
sayHi () {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
}
let person1 = new Person('Tom', 18)
person1.sayHi() // Tom 18
console.log(person1) // Person { name: 'Tom', age: 18 }
//定义一个子类
class Human extends Person {
constructor(name, age, height) {
//通过super调用父类的构造方法
super(name, age)
this.height = height
}
//在子类自身定义方法
sayHi () {
console.log("调用子类的方法")
console.log(this.name, this.age, this.height);
}
}
let H1 = new Human('Jerry', 18, 180)
console.log(H1) //Human { name: 'Jerry', age: 18, height: 180 }
H1.sayHi() //Jerry 18 180
  • ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面Parent.apply(this)
  • ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。


参考文章: