Class 可以通過extends
關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
class Point {
}
class ColorPoint extends Point {
}
上面代碼定義了一個ColorPoint
類,該類通過extends
關鍵字,繼承了Point
類的所有屬性和方法。但是由於沒有部署任何代碼,所以這兩個類完全一樣,等於複製了一個Point
類。下面,我們在ColorPoint
內部加上代碼。
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
方法,否則新建實例時會報錯。這是因爲子類自己的this
對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法。如果不調用super
方法,子類就得不到this
對象。
父類的靜態方法,也會被子類繼承。
Object.getPrototypeOf
方法可以用來從子類上獲取父類。
super
這個關鍵字,既可以當作函數使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
第一種情況,super
作爲函數調用時,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次super
函數。
class A {}
class B extends A {
constructor() {
super();
}
}
上面代碼中,子類B
的構造函數之中的super()
,代表調用父類的構造函數。這是必須的,否則 JavaScript 引擎會報錯。
注意,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 {}
class B extends A {
m() {
super(); // 報錯
}
}
上面代碼中,super()
用在B
類的m
方法之中,就會造成句法錯誤。
第二種情況,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();
上面代碼中,屬性x
是定義在A.prototype
上面的,所以super.x
可以取到它的值。
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();
上面代碼中,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
的實例。
注意,使用super
的時候,必須顯式指定是作爲函數、還是作爲對象使用,否則會報錯。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 報錯
}
}
上面代碼中,console.log(super)
當中的super
,無法看出是作爲函數使用,還是作爲對象使用,所以 JavaScript 引擎解析代碼的時候就會報錯。這時,如果能清晰地表明super
的數據類型,就不會報錯。
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
上面代碼中,super.valueOf()
表明super
是一個對象,因此就不會報錯。同時,由於super
使得this
指向B
的實例,所以super.valueOf()
返回的是一個B
的實例。
最後,由於對象總是繼承其他對象的,所以可以在任意一個對象中,使用super
關鍵字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
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) {
// ...
}