ES6的Class類繼承, super關鍵字用法即注意點

Class 的繼承

1.0 簡介

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方法中調用super方法,否則新建實例時會報錯。這是因爲子類自己的this對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法。如果不調用super方法,子類就得不到this對象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代碼中,ColorPoint繼承了父類Point,但是它的構造函數沒有調用super方法,導致新建實例時報錯。

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方法才能調用父類實例。

也就是說, super() 方法必須在 constructor 方法體內的最上面調用

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; // 正確
  }
}

父類的靜態方法, 也會被子類繼承

父類的類上的普通方法的繼承

看以下案例:

class Animal {
    eat = this.eat // 父類的類上的方法(也就是父類的原型上的方法)不會被繼承, 除非添加到實例 this上
    constructor(name, age, sex) {
        this.name = name
        this.age = age
        this.sex = sex
    }

    sayHello() { // 父類的類上的普通方法不能被繼承
        console.log('父類的類上的普通方法sayHello...');
    }
    eat(){
        console.log('父類類上的普通方法, 顯示添加到實例原型this 上 eat...');
    }
    static staticFn(){
        console.log('父類的靜態方法,可以被繼承,但是隻能被子類調用, 不能被子類實例調用 staticFn...');
    }
   
}

class Dog extends Animal {
    constructor(name, age, sex, color) {
        super(name, age, sex)
        this.color = color
    }
    sayHi() {
        console.log('-------- Dog類', this)
    }
}

const dog = new Dog('d', 12, 'b', 'yellow')
dog.sayHi()
dog.eat() // 父類的方法添加到 this 實例上之後可以被繼承
Dog.staticFn() // 父類的靜態方法可以被繼承, 但是隻能子類的類調用
dog.sayHello() // 父類的類上普通方法能被繼承, 但是不在子類 this實例上

2.0 Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用來從子類獲取父類

class Animal {
    constructor(name, age, sex) {
        this.name = name
    }

    sayHello() { // 父類的類上的普通方法不能被繼承
        console.log('父類的類上的普通方法sayHello...');
    }
}

class Dog extends Animal {
    constructor(name, age, sex, color) {
        super(name, age, sex)
        this.color = color
    }
}

# console.log(Object.getPrototypeOf(Dog) === Animal); // true

因此,可以使用這個方法判斷,一個類是否繼承了另一個類。

3. super 關鍵字

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)

super()內部的this指向的是B

作爲函數時, super 只能用在子類的構造函數中, 用在其他地方會報錯.

class A {}

class B extends A {
  m() {
    super(); // 報錯
  }
}

上面代碼中,super()用在B類的m方法之中,就會造成語法錯誤。

第二種情況: super 作爲對象時.

  1. 在普通方法中, 指向父類的原型對象.
  2. 在靜態方法中, 指向父類.
class Animal {
    constructor() {
    }
    sayHiParent(){
        console.log('我是父類的普通方法... ...');
    }
}

class Dog extends Animal {
    constructor() {
        super()
    }
   sayHi(){
        super.sayHiParent() // 我是父類的普通方法
       Animal.prototype.sayHiParent() // // 我是父類的普通方法
   }
}
const dog = new Dog()
dog.sayHi()

上面代碼中, 子類 Dog 當中的 super.sayHiParent(), 就是將 super 當做一個對象使用, 此時, super 在普通方法之中, 指向A.prototype, , 所以super.sayHiParent() 就相當於 Animal.prototype.sayHiParent()

這裏需要注意: 由於 super 指向父類的原型對象, 所以定義在父類實例上的方法或屬性, 是無法通過 super 調用的.

class Animal {
    constructor() {
        this.name = 'super實例上的屬性'
    }
}

class Dog extends Animal {
   sayHi(){
       return super.name
   }
}
const dog = new Dog()
console.log(dog.sayHi()); // undefined 所以定義在父類實例上的屬性, 無法通過super調用

在上面代碼中 name 是父類 Animal 實例的屬性, super.name 就引用不到它.

如果屬性定義在父類的原型對象上, super 就可以取到

class Animal {

}

Animal.prototype.name = 'super實例上的屬性'

class Dog extends Animal {
    sayHi() {
        return super.name
    }
}

const dog = new Dog()
console.log(dog.sayHi()); // super實例上的屬性

上面代碼中, 屬性 name 是定義在 Animal.prototype 上面上的, 所以 super.name 可以取到它的值.

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); // super在靜態方法中指向父類, 而不是父類的原型
    }

    myMethod(msg) {
        super.myMethod(msg); // super在普通方法中指向父類的原型
    }
}

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的實例。

4.0 類的 prototype屬性和__proto__屬性

大多數瀏覽器的 ES5 實現之中, 每一個對象都有__proto__屬性, 指向對一個的構造函數的prototype屬性, Class 作爲構造函數的語法糖, 同時有prototype屬性和proto 屬性, 因此同時存在兩條繼承鏈

  1. 子類的 __proto__ 屬性, 表示構造函數的繼承, 總是指向父類.
  2. 子類的 prototype 屬性的__proto__ 屬性, 表示方法的繼承, 總是指向父類的 prototype 屬性.
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代碼中,子類B__proto__屬性指向父類A,子類Bprototype屬性的__proto__屬性指向父類Aprototype屬性。

這樣的結果是因爲,類的繼承是按照下面的模式實現的。

class A {
}

class B {
}

// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);

const b = new B();

這兩條繼承鏈,可以這樣理解:

  • 作爲一個對象,子類(B)的原型(__proto__屬性)是父類(A);
  • 作爲一個構造函數,子類(B)的原型對象(prototype屬性)是父類的原型對象(prototype屬性)的實例。

4.1 實例的 __proto__ 屬性

子類實例的 __proto__ 屬性的 __proto__屬性, 指向父類實例的__proto__ 屬性, 也就是說, 子類的原型的原型, 就是父類的原型.

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

總結:

上一章: ES6類Class 的基本語法, 靜態方法, 實例屬性新寫法

下一章:

交流學習添加微信(備註技術交流學習): Gene199302
在這裏插入圖片描述

該博客爲學習阮一峯 ES6入門課所做的筆記記錄, 僅用來留作筆記記錄和學習理解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章