class
首先, 在
JavaScript
中,class
類是一種函數
class User {
constructor(name) { this.name = name; }
sayHi() {alert(this.name);}
}
alert(typeof User); // function
class User {…} 構造器內部干了啥?
- 創建一個以
User
為名稱的函數, 這是類聲明的結果(函數代碼來自constructor
中) - 儲存所有方法, 例如
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();
兩者存在重大差異
-
首先,通過
class
創建的函數是由特殊內部屬性標記的[[FunctionKind]]:"classConstructor"
。不像普通函數,調用類構造器時必須要用new
關鍵詞:class User {
constructor() {}
}
alert(typeof User); // function
User(); // Error: 沒有 ‘new’ 關鍵詞,類構造器 User 無法調用此外,大多數 JavaScript 引擎中的類構造函數的字符串表示形式都以 “class” 開頭
class User {
constructor() {}
}
alert(User); // class User { ... } -
方法不可枚舉。 對於
"prototype"
中的所有方法,類定義將enumerable
標記為false
。這很好,因為如果我們對一個對象調用 for..in 方法,我們通常不希望 class 方法出現。
枚舉實例屬性時, 不會出現class方法; 而普通創建的構造函數, 枚舉實例屬性時會出現prototype上的方法。
-
類默認使用
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 < 0) throw 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 < 0) throw new Error("Negative water");
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
雖然這看起來有點長,但函數更靈活。他們可以接受多個參數(即使我們現在不需要它們)// 更加靈活,原來getter中不能加參數,setter中只可以加一個參數,newVal,但是使用了函數后可以自定義加任意的參數
類聲明在 User.prototype
中創建 getters
和setters
,示例:
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]]
的引用
Rabbit
方法原型繼承自Animal
方法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.prototype
到Animal.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
關鍵字提供了上述功能
- 執行
super.method(…)
調用父類方法; (借用並改造父類方法, 生成自己的方法) - 執行
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 方法
}
}
因此,箭頭函數中的super
與stop()
中的是相同的,所以它能按預期工作。但如果我們在這里指定一個"普通"函數,那么將會拋出錯誤: (找不到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
,而並沒有在原型鏈上向上尋找方法。
-
在
longEar.eat()
中,(**)這一行調用rabbit.eat
並且此時this=longEar
。// 在 longEar.eat() 中 this 指向 longEar
this.__proto__.eat.call(this) // (**)
// 變成了
longEar.__proto__.eat.call(this)
// 即等同於
rabbit.eat.call(this); -
之后在
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); -
…所以
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 = {
eat: function() { // eat() {...}
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
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
的方法從一個對象復制到另一個對象是不安全的。
補充:
- 箭頭函數沒有自己的
this
或super
,所以它們能融入到就近的上下文,像透明似的。
class Rabbit
與class 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({a: 1, b: 2}) ); // 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({a: 1, b: 2})); // Error
順便說一下,Function.prototype
也有一些函數的通用方法,比如 call
、bind
等等。在上述的兩種情況下他們都是可用的,因為對於內置的 Object
構造函數來說,Object.__proto__ === Function.prototype
。(所有函數都是默認如此)
因此class Rabbit
與class Rabbit extends Object
有兩點區別
class Rabbit | class Rabbit extends Object |
---|---|
- | needs to call super() in constructor |
Rabbit.__proto__ === Function.prototype |
Rabbit.__proto__ === Object |