1. 程式人生 > >ES6中Class的繼承 學習筆記

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方法

,否則新建例項時會報錯。這是因為子類自己的this物件,必須先通過父類的建構函式完成塑造,得到與父類同樣的例項屬性和方法,然後再對其進行加工,加上子類自己的例項屬性和方法。如果不呼叫super方法,子類就得不到this物件。

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) {
  // ...
}