JS 继承
本文结合原先写过的JS 面向对象,接着写 JS 中实现继承的几种方式;
原型链继承
JS 是一种基于原型的语言,链式继承是这个语言很常见的继承方式;涉及到三种对象,实例对象、构造函数、原型对象,其中构造函数有一个原型对象,原型对象有一个指针指向构造函数,实例对象有一个指针指向原型对象。
用代码来表示就是:
1
2
3
4
5
6
7
8
// 实例
instance.__proto__ === prototype;
// 构造函数
constructor.prototype === prototype;
// 原型对象
prototype.constructor === constructor;
看个具体例子:
1
2
3
4
5
6
7
8
9
10
11
12
function A() {
this.name = 'A';
this.arr = [1, 2, 3];
}
function B() {
this.name = 'B';
}
/*
* 使用原型链实现继承
*/
B.prototype = new A();
let b = new B();
通俗易懂,直接让子类的prototype
指针指向父类的实例对象,实现继承;
注意这里是父类的实例对象,为什么不直接指向父类呢,这是因为父类是个构造函数,如果直接指向父类,那继承的其实只是这个构造函数,但构造函数对象上本身没有任何值得继承的属性或者方法,因为构造函数内部用的this
指的是实例对象;
基于原型的继承会产生一些问题,由于继承的是父类的实例对象,所以如果某个属性是引用类型,那子类的所有实例的该属性都会指向同一个引用地址,像上面的例子;
1
2
3
let b1 = new B(),
b2 = new B();
b1.arr === b2.arr; // true
基于构造函数的继承(call)
1
2
3
4
5
6
7
8
9
10
11
12
13
function A() {
this.name = 'A';
this.arr = [1, 2, 3];
}
A.prototype.method = function () {};
function B() {
// 使用call调用父类A,使B的实例也有A实例的属性,并且每个B的实例属性都是不一样的引用地址
A.call(this);
this.name = 'B';
}
let b = new B();
// 但由于B没有真正的继承A,所以实例b访问不到A原型链上的方法method
b.method(); // 报错
基于构造函数的继承,核心是在子类内部,通过call
方法,执行父类的构造函数,但是和new
操作不一样咯,只是在子类的实例上添加属性,并没有继承父类;所以其实会导致实例访问不到父类原型链上的属性。
组合式继承(原型链+构造函数)
结合前两种方式,实现的继承;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function A() {
this.name = 'A';
this.arr = [1, 2, 3];
}
A.prototype.method = function () {};
function B() {
// 使用call调用父类A,使B的实例也有A实例的属性,并且每个B的实例属性都是不一样的引用地址
A.call(this);
this.name = 'B';
}
/*
* 使用原型链实现继承
*/
B.prototype = new A();
// 手动改变指针指向
B.prototype.constructor = B;
let b = new B();
b.method();
我们前面说过,原型对象上有一个指针指向构造函数,在例子中B
是构造函数,所以我们让B.prototype
原型对象的construtor
指针指向构造函数B
本身。这样就实现了一个双向的指向问题,可以通过原型对象访问到构造函数,也可以通过构造函数来访问原型对象了。
继承普通对象(原型链方式)
前面讲了有关构造函数的继承方式,都涉及到了prototype
这个属性,这是函数特有的一个属性,如果想对普通对象继承,就需要引入Object.create
这个函数;该函数接受一个原型对象,一个属性类型对象(会添加到要返回的新对象上),返回一个新对象,新对象的__proto__
属性指向原型对象。
但是要注意使用这个方法得到的新对象,是原对象属性的浅拷贝,因为只实现了原型链继承,说白了就是把新对象的原型指向了原型对象,并不会像构造函数那样在对象上添加属性。
1
2
3
4
5
6
7
8
let parent = {
a: 1,
b: [1, 2, 3],
method() {}
};
let c1 = Object.create(parent);
let c2 = Object.create(parent);
c1.b === c2.b; // true
寄生继承
这种继承方式只是在使用Object.create
的原型继承方式上,手动添加了额外的方法;看个例子就明白了。
1
2
3
4
5
6
7
8
9
10
11
12
13
let parent = {
a: 1,
b: [1, 2, 3],
method() {}
};
const clone = parent => {
let child = Object.create(parent);
child.method2 = function () {
// do something
};
return child;
};
let child = clone(parent);
像这样创建了一个函数,在函数内部对要返回的对象进行一些操作,就能达到这种继承方式了。不过这种方式看起来不是很好用,使用场景也不是很多吧。
寄生组合式继承(终极方案)
前几种方案或多或少都有些问题,寄生组合式方案算是最优解了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function parent() {
this.name = 'a';
this.arr = [1, 2, 3];
}
parent.prototype.method = () => {};
function child() {
// 构造函数式继承,借助call方法
parent.call(this);
this.attr = 123;
}
function clone(child, parent) {
// 使用原型链继承时原本要用new关键词,
// 会使得parent构造函数多执行一次,
// 这里用Object.create方法避免了
child.prototype = Object.create(parent);
child.prototype.constructor = child;
}
clone(child, parent);
child.prototype.method = () => {};
let c = new child();
寄生组合式继承减少了调用父类的次数,在性能上更有优势。总的来说可以归纳为:使用call
(或者apply
)实现构造函数式继承,再写克隆函数,在其内部调用Object.create
方法,实现原型链式继承;
class extends
分析
ES6 中实现了class, extends
语法糖,来看看它编译为 ES5 之后的结果;
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ES6
class Person {
constructor(name) {
this.name = name;
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name);
};
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name);
this.age = age;
}
}
const asuna = new Gamer('Asuna', 20);
asuna.getName(); // 成功访问到父类的方法
// ES5
function _possibleConstructorReturn(self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
// 继承函数,相当于clone
function _inherits(subClass, superClass) {
// 这里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : (subClass.__proto__ = superClass);
}
var Parent = function Parent() {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child() {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
})(Parent);
可以看到语法糖编译后使用的也是组合寄生的继承方式;