javascript中实现继承的几种方式

本文介绍在 javascript 中如何实现继承,从 es5 到 es6 的各个实现方式。

绑定父类构造函数

仅继承父类构造函数内的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person( name , age ){
this.name = name
this.age = age
this.play = function(){
console.log('i can play!')
}
}

Person.prototype.say = function(){
console.log('hello i am' + this.name )
}

function Man(){
// 调用父类的构造函数
Person.apply( this , arguments )
}

var p = new Man('hong', 18)

console.log( p.name , p.age ) // hong 18
p.play() // i can play!
p.say() // Uncaught TypeError: p.say is not a function

如上代码,我们在子类 Man 的构造函数内部使用了 apply 的方式调用了父类 Person 的构造方法,使得 Person 中的 this 指向 Man 的实例对象,从而实现了对 Person 构造函数内属性和方法的继承。但是无法继承 Person 原型上的属性和方法。

来自父类原型

将父类的原型转接给子类的方式。

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
function Person( name , age ){
this.name = name
this.age = age
this.play = function(){
console.log('i can play!')
}
}

Person.prototype.say = function(){
console.log('hello i am a person')
}

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

Man.prototype = new Person()
Man.prototype.constructor = Man // 修复原型链指向
// Man 自定义的方法或属性
Man.prototype.foo = function(){
console.log('this is foo func in Man')
}

var p = new Man('小红', 14)
console.log( p.name , p.age ) // 小红 14
p.say() // hello i am a person
p.foo() // this is foo func in Man
p.play() // Uncaught TypeError: p.play is not a function

如上代码,我们为了实现原型继承,将父类 Person 的实例对象赋值给了 Man.prototype 这时候 Man 便拥有了 Person 原型上的所有属性和方法。
但是要注意,为了不破坏 Man 的原型链,必须将 Man.prototype.constructor 指向 Man。此时,我们没有继承到父类 Person 构造函数内的属性及方法。

组合继承

通过上面的两种方式,我们已经可以继承到父类构造函数中的属性和方法,以及父类原型上的属性和方法了,那么,现在我们将其组合处理一下。

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 Person( name , age ){
this.name = name
this.age = age
this.play = function(){
console.log('i can play!')
}
}

Person.prototype.say = function(){
console.log('hello i am' + this.name)
}

function Man(){
Person.apply( this , arguments )
}

Man.prototype = new Person()
Man.prototype.constructor = Man

var p = new Man('小红', 14)
console.log( p.name , p.age ) // 小红 14
p.play() // i can play!
p.say() // hello i am小红

经过如上处理,我们已经实现了对 Person 的非静态继承(继承自构造函数内属性和方法及原型上的属性和方法)。

Object.create 方法

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

语法

1
Object.create(proto[, propertiesObject])

参数

proto

新创建对象的原型对象。

propertiesObject

可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties() 的第二个参数。

返回值

一个新对象,带着指定的原型对象和属性。

例子

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

Person.prototype.say = function(){
console.log('hello')
}

var p = Object.create(new Person('hong'))

p.say() // hello

如上代码,通过 Object.create 创建的新对象 p 拥有了 Person 的原型方法和属性。

寄生组合继承

现在让我们把所有的知识点结合起来,优化继承的实现。

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
/**
* 寄生组合继承的核心代码
* @param {Function} sub 子类
* @param {Function} parent 父类
*/
function inheritPrototype(sub, parent) {
// 拿到父类的原型
var prototype = Object.create(parent.prototype);
// 改变constructor指向
prototype.constructor = sub;
// 父类原型赋给子类
sub.prototype = prototype;
}

function Person(name) {
this.name = name;
}

Person.prototype.func = function() {
console.log("Person");
};

function Man() {
// 调用父类的构造函数
Person.apply(this, arguments);
}

inheritPrototype(Man, Person);

var p = new Man("小红");
p.func(); // Person
console.log(p.name); // 小红

es6中的继承

class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
class Person {
}

class Man extends Person {
}

上面代码定义了一个 Man 类,该类通过 extends 关键字,继承了 Person 类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个 Person 类。下面,我们在Man 内部加上代码。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Person {
// 构造方法
constructor( name , age ){
this.name = name
this.age = age
}

// 原型方法
say(){
console.log('hello i am' + this.name + '(Preson)' )
}

// 静态方法
static foo(){
console.log('this is a static method foo')
}


}

class Man extends Person {
constructor(name, age , height){
// 调用父类的构造方法
super(name, age)
// 之后,添加自身的实例属性或方法
this.height = height
}

// 自身的挂载在原型上的方法
play(){
console.log('i can play')
}

// 如果子类中有与父类相同的属性或方法,则优先调用子类的属性或方法
// say(){
// console.log('hello i am' + this.name + '(Man)' )
// }

// 自身的静态方法
static bar(){
console.log('this is a static method bar')
}

}


let p = new Man('小红', 14, 188)

console.log( p.name , p.age , p.height ) // 小红 14 188
p.play() // i can play
Man.bar() // this is a static method bar
p.say() // hello i am小红(Preson)
Man.foo() // this is a static method foo

如上代码,我们可以对父类的 实例属性和方法静态属性和方法以及原型属性和方法 进行继承,并且语法上更为清晰简单。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

1
2
3
4
5
6
7
8
class Point { /* ... */ }

class ColorPoint extends Point {
constructor() {
}
}

let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint 继承了父类 Point,但是它的构造函数没有调用 super 方法,导致新建实例时报错。

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

super关键字

super 这个关键字,既可以当作 函数 使用,也可以当作 对象 使用。在这两种情况下,它的用法完全不同。

  1. 作为函数时

super 作为函数调用非时,表示父类的构造函数 Parent.prototype.constructor,es6 规定,在使用 extends 实现继承时,必须先在子类的构造函数中优先调用一次 super(),否则直接使用 this 会报错。并且 super 作为函数调用时,只能出现在子类的构造函数中,用在其他地方会报错。

  1. 作为对象时

super 作为对象调用时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例。

另外,在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。

注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

1
2
3
4
5
6
7
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

上面代码中,c 对象是 a 对象和 b 对象的合成,具有两者的接口。

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

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 mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}

for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}

return Mix;
}

function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}

上面代码的 mix 函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

1
2
3
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}