[ES6]ES6語法中的class、extends與super的原理


class

首先, 在JavaScript中, class類是一種函數

class User {
    constructor(name) { this.name = name; }
    sayHi() {alert(this.name);}
}

alert(typeof User); // function

class User {…} 構造器內部干了啥?

  1. 創建一個以User為名稱的函數, 這是類聲明的結果(函數代碼來自constructor中)
  2. 儲存所有方法, 例如User.prototype中的sayHi
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

class並不是JavaScript中的語法糖, 雖然我們可以在沒有 class 的情況下聲明同樣的內容:

// 以純函數的重寫 User 類

// 1. 創建構造器函數
function User(name{
     this.name = name;
}
// * 任何函數原型默認具有構造器屬性,
// 所以,我們不需要創建它

// 2. 向原型中添加方法
User.prototype.sayHi = function() {
    alert(this.name);
};

// 使用方法:
let user = new User("John");
user.sayHi();
兩者存在重大差異
  1. 首先,通過 class 創建的函數是由特殊內部屬性標記的 [[FunctionKind]]:"classConstructor"。不像普通函數,調用類構造器時必須要用 new 關鍵詞:

    class User {
       constructor() {}
    }

    alert(typeof User); // function
    User(); // Error: 沒有 ‘new’ 關鍵詞,類構造器 User 無法調用

    此外,大多數 JavaScript 引擎中的類構造函數的字符串表示形式都以 “class” 開頭

    class User {
     constructor() {}
    }

    alert(User); // class User { ... }
  2. 方法不可枚舉。 對於 "prototype" 中的所有方法,類定義將 enumerable 標記為false

    這很好,因為如果我們對一個對象調用 for..in 方法,我們通常不希望 class 方法出現。

    枚舉實例屬性時, 不會出現class方法; 而普通創建的構造函數, 枚舉實例屬性時會出現prototype上的方法。

  3. 類默認使用 use strict。 在類構造函數中的所有方法自動使用嚴格模式。

Getters/setters 及其他 shorthands

就像對象字面量,類可能包括 getters/setters,generators,計算屬性(computed properties)等。

使用 get/set 實現 user.name 的示例:

class User {

  constructor(name) {
    // 調用 setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name too short.

除了使用getter/setter語法,大多數時候我們首選 get…/set… 函數

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0throw new Error("Negative water");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().waterAmount = 100// setter 賦值函數
class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0throw new Error("Negative water");
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

雖然這看起來有點長,但函數更靈活。他們可以接受多個參數(即使我們現在不需要它們)// 更加靈活,原來getter中不能加參數,setter中只可以加一個參數,newVal,但是使用了函數后可以自定義加任意的參數

類聲明在 User.prototype 中創建 getterssetters,示例:

Object.defineProperties(User.prototype, {
  name: {
    get() {
      return this._name
    },
    set(name) {
      // ...
    }
  }
});
class屬性
class User {
  name = "Anonymous";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi();

屬性不在 User.prototype 內。相反它是通過 new 分別為每個對象創建的。所以,該屬性永遠不會在同一個類的不同對象之間共享。

總結

基本的類語法:

class MyClass {
    prop = value;  // filed 公有字段聲明(通過new分別為每個對象創建)
    #prop = value; // field 私有字段聲明(從類外部引用私有字段是錯誤的。它們只能在類里面中讀取或寫入。)

    static prop = value; // 靜態屬性(存儲類級別的數據,MyClass本身的屬性, 而不是定義在實例對象this上的屬性, 只能通過 MyClass.prop 訪問);靜態屬性是繼承的。

    constructor(...) { // 構造器
        // ...
    }

    method(...) {} // 方法

    static method(...) {} // 靜態方法被用來實現屬於整個類的功能,不涉及到某個具體的類實例的功能;靜態方法是繼承的;

    get something(...) {} // getter 方法
    set something(...) {} // setter 方法

    [Symbol.iterator]() {} // 計算 name/symbol 名方法 // 變量做屬性
}

由於extends創建了兩個[[prototype]]的引用

  1. Rabbit方法原型繼承自Animal方法
  2. Rabbit.prototype 原型繼承自Animal.prototype

Rabbit.__proto__ === Animal,因此對於class B extends A,類B的prototype指向了A,所以如果一個字段在B中沒有找到,會繼續在A中查找。故而靜態屬性和方法都是被繼承的

技術上來說,靜態聲明等同於直接給類本身賦值:

class MyClass {
  static property = ...;

  static method() {
    ...
  }
}
// 等同於
MyClass.property = ...
MyClass.method = ...

實例屬性的新寫法:

實例屬性除了定義在constructor()方法里面的this上面,也可以定義在類的最頂層

class IncreasingCounter {
  constructor() {
    this._count = 0// (*)
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代碼中,實例屬性this._count定義在constructor()方法里面。另一種寫法是,這個屬性也可以定義在類的最頂層,其他都不變。

class IncreasingCounter {
  _count = 0// (**)
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代碼中,實例屬性_count與取值函數value()increment()方法,處於同一個層級。這時,不需要在實例屬性前面加上this

這種新寫法的好處是,所有實例對象自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些實例屬性

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
  }
}

上面的代碼,一眼就能看出,foo類有兩個實例屬性,一目了然。另外,寫起來也比較簡潔。

extends

根據規范,如果一個類繼承了另一個類並且沒有constructor,那么將生成以下"空" constructor:

class Rabbit extends Animal {
    // 為沒有構造函數的繼承類生成以下的構造函數
    constructor(...ars) {
        super(...args);
    }
}
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }
}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbit = new Rabbit("White Rabbit");
console.log(rabbit); // console: Rabbit {speed: 0, name: "White Rabbit"}
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

extends干了啥?

通過指定"extends Animal"讓 Rabbit繼承自 Animal

Rabbit內部,extends關鍵字添加了[[Prototype]]引用: 從 Rabbit.prototypeAnimal.prototype

`extends`允許后接任何表達式(高級編程模式中用到)

類語法不僅可以指定一個類,還可以指定extends之后的任何表達式

ex.一個生成父類的函數調用

function f(phrase{
    return class {
        sayHi() { alert(phrase) }
    }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

這里是 class User繼承自f("Hello")的結果

我們可以根據多種狀況使用函數生成類,並繼承它們,這對於高級編程模式來說可能很有用。

super

通常來說,我們不希望完全替換父類的方法,而是希望基於它做一些調整或者功能性的擴展。我們在我們的方法中做一些事情,但是在它之前/之后或在執行過程中調用父類方法。

super關鍵字提供了上述功能

  1. 執行 super.method(…)調用父類方法; (借用並改造父類方法, 生成自己的方法)
  2. 執行super(…)調用父類構造函數(只能在子類的構造函數中運行) (繼承父類屬性)
重寫原型方法
class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  stop() { // (*)
    super.stop(); // 調用父類的 stop 函數
    this.hide();  // 然后隱藏
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

箭頭函數沒有super

如果箭頭函數中,super被訪問,那么則會從外部函數中獲取(類似this)

class Rabbit extends Animal {
    stop() {
        setTimtout(() => super.stop(), 1000); // 1 秒后調用父類 stop 方法
    }
}

因此,箭頭函數中的superstop()中的是相同的,所以它能按預期工作。但如果我們在這里指定一個"普通"函數,那么將會拋出錯誤: (找不到super)

class Rabbit extends Animal {
  stop() {
    setTimeout(function () super.stop() }, 1000); // Unexpected super
  }
}

代碼解析會出錯,報Uncaught SyntaxError: 'super' keyword unexpected here

重寫構造函數

根據 規范,如果一個類繼承了另一個類並且沒有 constructor,那么將生成以下“空” constructor

class Rabbit extends Animal {
    // 為沒有構造函數的繼承類生成以下的構造函數
    constructor(...args) {
        super(...args);
    }
}

可以看到,它調用了父類的constructor, 並傳遞了所有的參數。

如果給繼承類添加一個自定義的額構造函數

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 不生效!
let rabbit = new Rabbit("White Rabbit"10); 

報錯: Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

解釋下就是: 繼承類的構造函數必須調用 super(...), 並且一定要在this之前調用

這是因為, 在JavaScript中,“繼承類的構造函數" 與所有其他的構造函數之間存在區別。在繼承類中,相應的構造函數會被標記為特殊的的內部屬性[[ConstructorKind]]:"derived"

不同點在於:

  • 當一個普通構造函數執行時,它會創建一個空對象作為this並繼續執行。
  • 但是當繼承的構造函數執行時,它並不會做這件事。它期望父類的構造函數來完成這項工作。

因此,如果我們在繼承類中構建了自己的構造函數,我們必須調用super,因為如果不這樣的話this指向的對象不會被創建。並且會收到一個報錯。

正確的寫法;需要在使用this之前調用super()

class Rabbit extends Animal {
    constructor(name, earLength) {
        super(name);
        this.earLength = earLength;
    }
}
super內部探究: [[HomeObject]]

當一個對象方法運行時,它會將當前對象作為this,如果調用super.method(),它需要從當前的原型中調用method

super技術上的實現,首先會想到,引擎知道當前對象的this,因此它可以獲取父method作為this.__proto__.method。但這個解決方法是行不通的。

讓我們來說明一下這個問題。沒有類,為簡單起見,使用普通對象。

let animal = {
    name'Animal',
    eat() {
        alert(`${this.name} eats.`);
    }
};

let rabbit = {
    __proto__: animal,
    name'Rabbit',
    eat() {
        // 這是 super.eat() 可能運行的原因
        this.__proto__.eat.call(this); // (*)
    }
};

rabbit.eat(); // Rabbit eats

在(*)這一行,我們從原型animal,我們從原型animal上獲取eat方法,並在當前對象的上下文中調用它。注意, .call(this)在這里非常重要,因為簡單的調用this.__proto__.eat()將在原型的上下文中執行eat,而非當前對象。

上述代碼中,我們獲得了正確的父類方法。但如果在原型鏈上再添加一個額外的對象。這就不成立了

let animal = {
    name'Animal',
    eat() {
        alert(`${this.name} eats`);
    }
};

let rabbit = {
    __proto__: animal,
    eat() {
        this.__proto__.eat.call(this); // (*)
    }
};

let longEar = {
    __proto__: rabbit,
    eat() {
        this.__proto__.eat.call(this); // (**)
    }
};

longEar.eat(); // Error: Maxium call stack size exceeded
// InternalError: too much recursion

代碼無法運行;這是由於在()和(*)這兩行中,this的值都是當前對象(longEar)。

在()和(*)這兩行中,this.__proto__的值是完全相同的: 都是rabbit。在這個無限循環中,它們都調用了rabbit.eat,而並沒有在原型鏈上向上尋找方法。

  1. longEar.eat()中,(**)這一行調用rabbit.eat並且此時this=longEar

    // 在 longEar.eat() 中 this 指向 longEar
    this.__proto__.eat.call(this// (**)
    // 變成了
    longEar.__proto__.eat.call(this)
    // 即等同於
    rabbit.eat.call(this);
  2. 之后在rabbit.eat的(*)行中,我們希望將函數調用再原型鏈上向更高層傳遞,但是因為this=longEar,因此this.__proto__.eat又是rabbit.eat

    // 在 rabbit.eat() 中 this 依舊等於 longEar
    this.__proto__.eat.call(this// (*)
    // 變成了
    longEar.__proto__.eat.call(this)
    // 再次等同於
    rabbit.eat.call(this);
  3. …所以 rabbit.eat 不停地循環調用自己,因此它無法進一步地往原型鏈的更高層調用。

因此,super無法單獨使用this來解決

[[HomeObject]]

為了提供super的解決方法,javascript為函數額外添加了一個特殊的內部屬性: [[HomeObjext]]

當一個函數被定義為類或者對象方法時, 它的[[HomeObject]]屬性就成為那個對象。

然后super使用它來解析父類原型和它自己的方法。

let animal = {
    name'Animal',
    eat() { // animal.eat.[[HomeObject]] == animal // (3)
        alert(`${this.name} eats.`);
    }
};

let rabbit = {
    __proto__: animal,
    name'Rabbit',
    eat() {
        super.eat(); // rabbit.eat.[[HomeObject]] == rabbit
        // rabbit.eat.[[HomeObject]].__proto__.eat.call(this); // (2)
    }
};

let longEar = {
    __proto__: rabbit,
    name'Lonet Ear',
    eat() { // longEar.eat.[[HomeObject]] == longEar
        super.eat();
        // longEar.eat.[[HomeObject]].__proto__.eat.call(this); // (1)
    }
};

// 正常運行
longEar.eat(); // alert: Lonet Ear eats.

上述代碼按照預期運行,基於[[HomeObject]]運行機制。 像longEar.eat這樣的方法,知道[[HomeObejct]],並且從它的原型中獲取父類方法, 並沒有使用 this。( 調用順序(1) -> (2) -> (3) )

方法並不是"自由"的

通常函數都是"自由"的,並沒有綁定到javascript中的對象。因此,它們可以在對象之間賦值,並且用另外一個this調用它。
[[HomeObject]]的存在違反了這個原則,因為方法記住了它們的對象[[HomeObject]]不能被修改,所以這個綁定是永久的。

在javascript語言中[[HomeObject]]僅被用於super所以,如果一個方法不使用super,那么仍然可以被視為自由且可在對象之間復制。但在super中可能出錯。

let animal = {
  sayHi() {
    console.log(`I'm an animal`);
  }
};

let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    console.log("I'm a plant");
  }
};

let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();  // I'm an animal (?!?)

原因很簡單:

  • 在(*)行,tree.sayHi方法從rabbit復制而來。(可能是為了避免重復代碼)
  • 所以它的[[HomeObject]]rabbit,因為它是在rabbit中創建的。無法修改[[HomeObject]]
  • tree.sayHi()內具有super.sayHi()。它從rabbit中上溯,然后從animal中獲取方法。
方法, 不是函數屬性

[[HomeObject]] 是為類和普通對象中的方法定義的。但是對於對象來說,方法必須確切指定為 method(),而不是 "method: function()"

這個差別對我們來說可能不重要,但是對 JavaScript 來說卻是非常重要的。

下面的例子中,使用非方法(non-method)語句進行比較。[[HomeObject]] 屬性未設置,並且繼承不起作用:

let animal = {
    eatfunction() // eat() {...}
        // ...
    }
};

let rabbit = {
    __proto__: animal,
    eatfunction() {
        super.eat();
    }
}

rabbit.eat(); // 錯誤調用 super(因為這里並沒有 [[HomeObject]])

總結

1.擴展類: class Child extends Parent:

  • 這就意味着Child.prototype.proto將是Parent.prototype,所以方法被繼承

2.重寫構造函數:

  • 在使用this之前,我們必須在Child構造函數中將父構造函數調用為super()。( super(…)用來初始化繼承類構造函數里的 this值,相當於手動執行了 this = Reflect.construct(super.constructor, args, new.target))

3.重寫方法:

  • 我們可以在Child方法中使用super.method()來調用Parent方法;(通過方法的內部屬性[[HomeObject]]實現往原型鏈的更高層調用)

4.內部工作:

  • 方法在內部[[HomeObject]]屬性中記住它們的類/對象。這就是super如何解析父類方法的。
  • 因此,將一個帶有super的方法從一個對象復制到另一個對象是不安全的。

補充:

  • 箭頭函數沒有自己的thissuper,所以它們能融入到就近的上下文,像透明似的。

class Rabbitclass Rabbit extends Object的區別

extends語法會設置兩個原型: (結果就是,繼承對於常規的和靜態的方法都生效)

1.在構造函數的prototype之間設置原型(為了獲取實例方法)

2.在構造函數之間會設置原型(為了獲取靜態方法)

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true

// 所以現在 Rabbit 對象可以通過 Rabbit 訪問 Object 的靜態方法,如下所示:
class Rabbit extends Object {}

// 通常我們調用 Object.getOwnPropertyNames
alert ( Rabbit.getOwnPropertyNames({a1b2}) ); // a,b (*)

但是如果我們沒有聲明 extends Object,那么 Rabbit.__proto__ 將不會被設置為 Object。

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)
alert( Rabbit.__proto__ === Function.prototype ); // 所有函數都是默認如此

// 報錯,Rabbit 上沒有對應的函數
alert ( Rabbit.getOwnPropertyNames({a1b2})); // Error

順便說一下,Function.prototype 也有一些函數的通用方法,比如 callbind 等等。在上述的兩種情況下他們都是可用的,因為對於內置的 Object 構造函數來說,Object.__proto__ === Function.prototype。(所有函數都是默認如此)

因此class Rabbitclass Rabbit extends Object有兩點區別

class Rabbit class Rabbit extends Object
- needs to call super() in constructor
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM