注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:
阮一峰ES6的书籍中ES6 Class 的继承
我的ES6入门学习规划
ES6提供了跟其它语言如java比较相像的继承方式,但本质上还是原型继承。
ES5的继承方式
1 | function ClassA(sColor) { |
ES6继承的形式
跟别的语言一样,都使用extend关键字,后面接父类名称:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Point {
}
class ColorPoint extends Point {
}
//1
console.log(Point.prototype.constructor === Point);// true
//2
console.log(ColorPoint.prototype.constructor === ColorPoint);// true
//3
console.log(new ColorPoint() instanceof Point);// true
//4
console.log(new ColorPoint() instanceof ColorPoint);// true
//5
console.log(ColorPoint.prototype.__proto__ === Point.prototype);//true
//6
console.log(ColorPoint.__proto__ === Point);// true
这个例子采用ES6来编写继承,效果与ES5一样。 观察这个例子(或者是ES5的那个例子)中的6个console.log,了解它们各自代表的含义:
- 第1个与第2个描述的是类的构造函数与prototype之间的关联关系,这是js语言规定的;
- 第3个与第4个是继承的特征表现,instanceof说明了ColorPoint的实例与ColorPoint和Point类,属于同一条原型链;
- 第5个表示子类与父类的实例之间的继承,实例的原型链的就是这么构建起来的。通过实例的原型链查找一个实例属性,先从子类实例开始找,找不到就到子类实例原型上去找,如果子类实例原型上访问不到,则会到子类实例原型的原型上去查找,而子类实例原型的原型,正好是父类实例的原型。
- 第6个表示子类与父类的静态继承,类级别的原型链就是这么构建起来的。通过类的原型链查找一个类的属性,先从子类开始找,找不到就到子类的原型上去找,而子类的原型,正好是父类。
最后2个也等价于:1
2Object.getPrototypeOf(ColorPoint.prototype) === Point.prototype;
Object.getPrototypeOf(ColorPoint) === Point;
对比ES5和ES6的继承,发现效果是差不多的,但是ES5必须通过下面的hack方式才能将ES6的继承特性实现:1
2
3ClassB.prototype = new ClassA();
ClassB.prototype.constructor = ClassB;
ClassB.__proto__ = ClassA;
如果不是特别注意的话,这三个点很有可能会被忽略掉。 ES6的继承则可以把这一切做地比较完美妥当。 另外ES6毕竟是语言级别的继承方式,还有新的特性可以使用,比如super关键字,可以很方面地在子类或子类实例调用父类的行为方法,这个是ES5里面没有的。
理解JS的原型链
首先要理解什么是类的原型,什么是类实例的原型。类的原型指的是类这个函数本身的原型,类实例的原型是指通过调用类的构造函数创建的实例对象的原型。在进行面向对象编程的时候,大部分情况都是把行为部署到类的实例上面去,然后通过实例方法来调用,少部分情况也会把行为直接部署在类级别上,然后通过静态方法来调用。继承帮助我们解决基础性、重复性的行为封装,因为有前面2种部署方式,所以继承也有两条继承的线。JS是通过2条原型链来实现这两种继承的。
实例的原型链继承,最关键的是把握以下几点:
- 类实例.__proto__指向的是类实例的原型,也就是类.prototype
- 类.prototype也是一个对象,它也有__proto__属性,它会指向父类.prototype;如果没有父类,它会指向Object.prototype
- Object.prototype.__proto__为null
所以如果想弄清楚一个类的实例,它的原型链是怎么样的,把它这个类、父类、以及原型对象都找出来即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class A{
}
class B extends A{
}
let b = new B();
// b.__proto__指向类实例的原型,也就是B.prototype
console.log(b.__proto__ === B.prototype); // true
// B.prototype也是一个对象,它.__proto__会指向A.prototype
console.log(B.prototype.__proto__ === A.prototype); // true
// 因为上面的结论成立,也说明子类的原型是父类的一个实例
console.log(B.prototype instanceof A); // true
// A没有继承于别的类,A.prototype就是Object类的一个实例
console.log(A.prototype instanceof Object); // true
// A.prototype就是Object类的一个实例,所以A.prototype.__proto__指向的是Object类的原型
console.log(A.prototype.__proto__ === Object.prototype); // true
虽然前面说到一个类,如果没有父类,它的prototype的__proto__属性会指向Object.prototype,能说这个类就继承于Object类吗?不能,因为继承是同时满足实例继承和类的静态继承的,前面的结论只能说明一个独立的类,在类实例层面,继承于Object类,但是类的静态继承方面并不是的,通过后面的内容就能明白这点了。
类的原型链继承,也有以下几个要点:
- 类.__proto__,指向的是父类;如果没有父类,类.__proto__指向Function.prototype,因为所有的类,都是Function的实例;
- Function.prototype是所有函数的原型对象,它是Object类的一个实例,所以它的__proto__属性指向的是Object.prototype
- Object.prototype.__proto__为null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class A{
}
class B extends A{
}
// B.__proto__指向是父类,也就是A
console.log(B.__proto__ === A); // true
// A没有继承别的类,A仅仅是Function的一个实例,它的__proto__指向Function的原型
console.log(A.__proto__ === Function.prototype); // true
// 注意:A不是继承于Function,A是Function的一个实例,所以A.__proto__ === Function不成立
console.log(A.__proto__ === Function);// false
// Function.prototype是Object类的一个实例
console.log(Function.prototype instanceof Object); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
值得提醒的是,A类没有继承别的类,但也不是继承于Function,它是Function的一个实例,所以它的原型就是Function.prototype。 我原本认为应该要把Function.__proto__考虑进去,看看是会怎么继续这个原型链,但是因为A.__proto__指向的并不是Function,所以Function.__proto__不是A类静态继承原型链的一部分,不能在这里去研究。
类的实例继承和类的静态继承,到这里就已经描述地很详细了,只要不是讨论Function类,其它类,都可以根据前面的结论来摸清它和它实例的继承原型链。最后也可以看到,不管是实例继承还是类的静态继承,所有继承的终点是Object.prototype,Object.prototype的__proto__指向的是null,从一个对象查找一个属性,如果找不到,最终都会找到Object.prototype的__proto__为止,这也是为啥原型链如果太长会影响到性能的原因。
Function.__proto__
1 | console.log(Function instanceof Function); // true |
因为Function也是Function类的实例,所以Function的__proto__指向的也是Function类的原型,也就是Function.prototype。
super复用父类行为
在继承的场景中,子类在重写或自定义新的行为时,如果想复用父类的行为,需要使用ES6新使用的关键字super。 super既可以当做对象又可以当函数使用,它主要使用的位置可分为构造函数、实例方法和静态方法。 super在不同的位置,有完全不同的特性,所以需要掌握清楚,以便实际编码的时候减少未知的错误。
super作为函数调用
在子类构造函数中,可以把super当函数一样使用,它用来调用父类的构造函数,它在调用时,虽然是执行父类的构造函数,但是内部this绑定的是子类要构造的实例。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为:
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
同时ES6还有规定:
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错
- 如果子类没有定义constructor方法,那么会自动加一个构造函数,并在内部调用super
ES6这种super方式来复用父类构造函数的方式,比较简单好理解,这样的话,父类实例化过程中,得到的所有非原型的实例属性和实例方法,都能够在子类实例化的时候处理一遍,最后子类实例就完全地同步了父类实例所有非原型的实例成员拉。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
34class A {
_dis = 0;
constructor(x) {
this._x = x;
this.hi = function () {
console.log('test hi');
}
}
// _dis _x hi 这三个属性在A类实例化的时候都属于非原型的实例属性或方法
getDis() {
return this._dis;
}
// getDis 是原型上的实例方法
};
let a = new A();
console.log(a.hasOwnProperty('_dis'));// true
console.log(a.hasOwnProperty('_x'));// true
console.log(a.hasOwnProperty('hi'));// true
console.log(a.hasOwnProperty('getDis'));// false
class B extends A{
//默认会被添加一个构造函数,并调用super
}
let b = new B();
console.log(b.hasOwnProperty('_dis'));// true
console.log(b.hasOwnProperty('_x'));// true
console.log(b.hasOwnProperty('hi'));// true
console.log(b.hasOwnProperty('getDis'));// false
从上面的例子可以很清楚地看到,super为子类实例复制了父类实例相同的非原型的实例成员。
前面记录到super在调用过程里面,this绑定的是子类的实例,可以通过一个简单的例子来验证这一点:1
2
3
4
5
6
7
8
9
10
11
12
13
14
let tmp = null;
class A {
constructor() {
tmp = this;
}
};
class B extends A {
}
let b = new B();
b === tmp;
tmp作为一个全局变量,始终记录A类构造函数被调用过程中的this指向,最后b === tmp说明了B类实例化过程中,super调用时的this确实为B类实例。
1 |
|
上一篇笔记记录到如何通过new.target如果在父类构造函数中使用,那么在做子类实例化的时候,它会指向子类的构造函数,可以用下面的例子来印证:1
2
3
4
5
6
7
8
9
10
11
12class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
在子类构造函数中,把super当函数使用,是super关键字唯一被作为函数使用的场景,其它地方把它用作函数都会报错。
super作为对象使用
要区分两种场景,一种是在构造函数、实例方法或原型方法中使用super对象,另外一种是在静态方法中使用super对象。
在构造函数、或原型方法中使用super对象
在这个场景下,super对象指向为父类的原型对象,它的作用是帮助我们重用父类原型上的行为,尤其是在子类需要覆盖父类的同名方法的时候,特别有用。
所以可以在子类的构造函数中、原型方法中,通过super来访问父类原型对象上的属性或方法,且在super调用父类原型方法时,内部this会绑定为子类实例。由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。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
44class A {
_content = '';
_tag = 'A';
constructor(content) {
this._content = content;
}
render() {
return `<${this._tag}>${this._content}</${this._tag}>`;
}
}
A.prototype.id = 'a_pro';
class B extends A {
_tag = 'B';
constructor(content){
super(content);
this.test();
this.test2 = function () {
// 这里面无法使用super关键字的:ES6规定super只能用在对象的方法当中。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
}
}
test() {
//super作为对象在构造函数、原型方法中使用,指向的是父类原型对象
console.log(super.render === A.prototype.render); // true
//可以通过super调用父类原型方法,并且内部this绑定的是子类实例
console.log(super.render()); // <B>hello world</B>
//可以通过super访问到父类原型上定义的属性
console.log(super.id); // a_pro
//super指向的是父类的原型对象,所以无法取到父类的实例属性
console.log(super._content); // undefined
}
}
let b = new B('hello world');
b.test();
test方法最终会调用两次,一次是在构造函数中,一次是最后的主动调用,打印结果都是一样的。
有一种很特殊的情况,在ES6这本书里面有介绍:
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
1 | class A { |
上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
这个结果虽然是看到了,但是并没有把原因解释清楚,既然super指向的是父类原型对象,取值的时候是去原型对象找属性,赋值的时候为啥不是给原型对象赋值呢?除非说super的赋值行为,在ES6的内部做了处理,等同于方法调用,由于方法调用内部this都是指向子类实例,所以给super赋值,最后等同于给子类实例赋值。
从我的想法来说,上面属于一种比较奇怪的现象,在实际编程的时候,我们是不应该去给super赋值的,那意味着子类的行为有篡改父类行为的含义,这种方式是违背封装原则的。
在静态方法中使用super对象
在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class A {
x = 1;// 父类的实例属性x
static x = 2; // 父类的静态属性x
static getX() {// 父类的静态方法x
console.log(this.x);
}
}
class B extends A {
x = 3;// 子类的实例属性x
static x = 4; //子类的静态属性x
}
B.getX();// 4
其它补充
由于对象总是继承其他对象的,所以可以在任意一个字面量对象中,都能使用super关键字。
原生类的继承
1 | Boolean() |
ES6以前,无法继承这些原生类。ES6可以了。1
2
3
4
5
6
7
8
9
10
11
12class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
注意,继承Object的子类,有一个行为差异。1
2
3
4
5
6
7class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。
多重继承
mixin模式可以帮助我们实现多重继承,目前对多重继承在ES6中应用没有多大的兴趣,暂时只是把这个点留在这里备忘,将来有需要的时候可以考虑使用: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
28function 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);
}
}
}