ES6中Class的繼承 學習筆記
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 呼叫父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 呼叫父類的toString()
}
}
上面程式碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這裡表示父類的建構函式,用來新建父類的this物件。
子類必須在constructor方法中呼叫super方法
ES6的繼承與ES5的繼承的不同
ES5 的繼承,實質是先創造子類的例項物件this,然後再將父類的方法新增到this上面(Parent.apply(this)
)。ES6 的繼承機制完全不同,實質是先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。
如果子類沒有定義constructor方法,這個方法會被預設新增,程式碼如下。也就是說,不管有沒有顯式定義,任何一個子類都有constructor方法。
class ColorPoint extends Point {
}
// 等同於
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
另一個需要注意的地方是,在子類的建構函式中,只有呼叫super之後,才可以使用this關鍵字,否則會報錯。這是因為子類例項的構建,基於父類例項,只有super方法才能呼叫父類例項。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正確
}
}
下面是生成子類例項的程式碼。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
上面程式碼中,例項物件cp同時是ColorPoint和Point兩個類的例項,這與 ES5 的行為完全一致。
最後,父類的靜態方法,也會被子類繼承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
上面程式碼中,hello()是A類的靜態方法,B繼承A,也繼承了A的靜態方法。
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用來從子類上獲取父類。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
super 關鍵字
super這個關鍵字,既可以當作函式使用,也可以當作物件使用。在這兩種情況下,它的用法完全不同。
第一種情況,super作為函式呼叫時,代表父類的建構函式。ES6 要求,子類的建構函式必須執行一次super函式。
注意,super雖然代表了父類A的建構函式,但是返回的是子類B的例項,即super內部的this指的是B,因此super()在這裡相當於A.prototype.constructor.call(this)。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super(); // 作為函式只能用在此處,用在其他地方報錯
}
}
new A() // A
new B() // B
上面程式碼中,new.target指向當前正在執行的函式。可以看到,在super()執行時,它指向的是子類B的建構函式,而不是父類A的建構函式。也就是說,super()內部的this指向的是B。
第二種情況,super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
上面程式碼中,子類B當中的super.p(),就是將super當作一個物件使用。這時,super在普通方法之中,指向A.prototype,所以super.p()就相當於A.prototype.p()。
這裡需要注意,由於super指向父類的原型物件,所以定義在父類例項上的方法或屬性,是無法通過super呼叫的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
p是父類A例項的屬性,super.p就引用不到它。
如果屬性定義在父類的原型物件上,super就可以取到。
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
ES6 規定,在子類普通方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類例項。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
上面程式碼中,super.print()雖然呼叫的是A.prototype.print()
,但是A.prototype.print()
內部的this指向子類B的例項,導致輸出的是2,而不是1。也就是說,實際上執行的是super.print.call(this)
。
由於this
指向子類例項,所以如果通過super
對某個屬性賦值,這時super
就是this
,賦值的屬性會變成子類例項的屬性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
let aa = new A();
aa.x // 1
aa.__proto__.x // undefined
上面程式碼中,super.x賦值為3,這時等同於對this.x賦值為3。而當讀取super.x的時候,讀的是A.prototype.x,所以返回undefined。
如果super作為物件,用在靜態方法之中,這時super將指向父類,而不是父類的原型物件。
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
上面程式碼中,super在靜態方法之中指向父類,在普通方法之中指向父類的原型物件。
另外,在子類的靜態方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類,而不是子類的例項。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3; // 相當於靜態屬性
B.m() // 3
上面程式碼中,靜態方法B.m裡面,super.print指向父類的靜態方法。這個方法裡面的this指向的是B,而不是B的例項。
Mixin 模式的實現
Mixin 指的是多個物件合成一個新的物件,新物件具有各個組成成員的介面。它的最簡單實現如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
上面程式碼中,c物件是a物件和b物件的合成,具有兩者的介面。
下面是一個更完備的實現,將多個類的介面“混入”(mix in)另一個類。
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix.prototype, mixin); // 拷貝例項屬性
copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷貝原型屬性
}
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函式,可以將多個物件合成為一個類。使用的時候,只要繼承這個類即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}