JS继承的几种方式

许多OO语言都支持两种继承方式:

  • 接口继承
  • 实现继承

而 ECMA 只支持实现继承,主要依靠原型链来实现

原型链继承

核心思想:重写原型对象,代之以一个新类型的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent() {
this.name = 'Parent';
}

Parent.prototype.getName = function () {
console.log(this.name);
}

function Child() {}

// 继承
Child.prototype = new Parent();

const child1 = new Child();
child1.getName(); // Parent

const child2 = new Child();
child2.getName(); // Parent

优点:

  • 父类新增原型方法/属性,子类都能访问到
  • 简单,易于实现

缺点:

  • 引用类型的属性被所有实例共享
  • 创建子类实例时,不能像父类构造函数传参

借用构造函数继承(经典继承)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent(name) {
this.name = name;
this.color = ['red'];
}

function Child(name) {
Parent.call(this, name);
}

const child1 = new Child('Child1');
child1.color.push('green');
console.log(child1); // Child { name: 'Child1', color: [ 'red', 'green' ] }

const child2 = new Child('Child2');
child2.color.push('blue');
console.log(child2); // Child { name: 'Child2', color: [ 'red', 'blue' ] }

优点:

  • 避免了引用类型的属性被所有子类实例所共享
  • 可以在子类中向父类构造函数传参

缺点:

  • 无法实现函数复用
  • 在父类原型中定义的方法,对于子类不可见

组合继承

这种方式的核心思想在于:通过结合原型链继承和经典继承两种方式,融合了二者的优点

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
function Parent(name) {
this.name = name;
this.color = ['red'];
}

Parent.prototype.getName = function () {
console.log(this.name);
}

function Child(name, age) {
// 继承属性
Parent.call(this, name);
this.age = age;
}

// 继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child1 = new Child('Child1', 18);
child1.color.push('green');
console.log(child1); // Child { name: 'Child1', color: [ 'red', 'green' ], age: 18 }

const child2 = new Child('Child2', 17);
child2.color.push('blue');
console.log(child2); // Child { name: 'Child2', color: [ 'red', 'blue' ], age: 17 }

缺点:

  • 调用了两次父类构造函数

原型式继承

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}

const person = {
name: 'person',
friends: ['daisy', 'kelly']
}

const person1 = createObj(person);
const person2 = createObj(person);

person1.name = 'person1';
console.log(person1); // { name: 'person1' }

person2.friends.push('taylor');
console.log(person2); // {}

console.log(person); // { name: 'person', friends: [ 'daisy', 'kelly', 'taylor' ] }

缺点:

  • 包含引用类型的属性值始终会共享相应的值

寄生式继承

核心思想:创建一个仅用于封装继承过程的函数,并在该函数中以某种形式来增强对象,最后返回该对象(和原型式继承紧密相关)

1
2
3
4
5
6
7
8
9
10
11
12
function createObj(o) {
let clone = Object.create(o);
clone.sayHi = function() {
console.log('Hi');
}
return clone;
}

const person = {
name: 'person',
friends: ['daisy', 'kelly']
}

缺点:

  • 不能复用函数而降低效率
  • 无法传递参数

寄生组合式继承

核心思想:结合借用构造函数产地参数和寄生模式实现

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
32
33
34
35
36
37
38
39
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}

function inheritPrototype(child, parent) {
const prototype = createObj(parent.prototype); // 创建对象
prototype.constructor = parent; // 增强对象
child.prototype = prototype; // 指定对象
}

function Parent(name) {
this.name = name;
this.color = ['red'];
}

Parent.prototype.getName = function () {
console.log(this.name);
}

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

inheritPrototype(Child, Parent);

Parent.prototype.getAge = function () {
console.log(this.age);
}

const child1 = new Child('Child1', 18);
child1.color.push('green');
console.log(child1); // Parent { name: 'Child1', color: [ 'red', 'green' ], age: 18 }

const child2 = new Child('Child2', 17);
child2.color.push('blue');
console.log(child2); // Parent { name: 'Child2', color: [ 'red', 'blue' ], age: 17 }

优点:

  • 高效率:只调用了一次父类构造函数

参考

《JavaScript高级程序教程(第3版)》