我們還可以爲整個類分配壹個方法。這樣的方法被稱爲 靜態的(static)。
在壹個類的聲明中,它們以 static
關鍵字開頭,如下所示:
class User { static staticMethod() { alert(this === User); } } User.staticMethod(); // true
這實際上跟直接將其作爲屬性賦值的作用相同:
class User { } User.staticMethod = function() { alert(this === User); }; User.staticMethod(); // true
在 User.staticMethod()
調用中的 this
的值是類構造器 User
自身(“點符號前面的對象”規則)。
通常,靜態方法用于實現屬于整個類,但不屬于該類任何特定對象的函數。
例如,我們有對象 Article
,並且需要壹個方法來比較它們。
通常的解決方案就是添加 Article.compare
靜態方法:
class Article { constructor(title, date) { this.title = title; this.date = date; } static compare(articleA, articleB) { return articleA.date - articleB.date; } } // 用法 let articles = [ new Article("HTML", new Date(2019, 1, 1)), new Article("CSS", new Date(2019, 0, 1)), new Article("JavaScript", new Date(2019, 11, 1)) ]; articles.sort(Article.compare); alert( articles[0].title ); // CSS
這裏 Article.compare
方法代表“上面的”文章,意思是比較它們。它不是文章的方法,而是整個 class 的方法。
另壹個例子是所謂的“工廠”方法。
比如說,我們需要通過多種方式來創建壹篇文章:
通過用給定的參數來創建(title
,date
等)。
使用今天的日期來創建壹個空的文章。
……其它方法。
第壹種方法我們可以通過 constructor 來實現。對于第二種方式,我們可以創建類的壹個靜態方法來實現。
例如這裏的 Article.createTodays()
:
class Article { constructor(title, date) { this.title = title; this.date = date; } static createTodays() { // 記住 this = Article return new this("Today's digest", new Date()); } } let article = Article.createTodays(); alert( article.title ); // Today's digest
現在,每當我們需要創建壹個今天的文章時,我們就可以調用 Article.createTodays()
。再說明壹次,它不是壹個文章的方法,而是整個 class 的方法。
靜態方法也被用于與數據庫相關的公共類,可以用于搜索/保存/刪除數據庫中的條目, 就像這樣:
// 假定 Article 是壹個用來管理文章的特殊類 // 通過 id 來移除文章的靜態方法: Article.remove({id: 12345});
靜態方法不適用于單個對象
靜態方法可以在類上調用,而不是在單個對象上。
例如,這樣的代碼無法正常工作:
// ... article.createTodays(); /// Error: article.createTodays is not a function
最近新增的特性
這是壹個最近添加到 JavaScript 的特性。 示例可以在最近的 Chrome 工作。
靜態的屬性也是可能的,它們看起來就像常規的類屬性,但前面加有 static
:
class Article { static publisher = "Levi Ding"; } alert( Article.publisher ); // Levi Ding
這等同于直接給 Article
賦值:
Article.publisher = "Levi Ding";
靜態屬性和方法是可被繼承的。
例如,下面這段代碼中的 Animal.compare
和 Animal.planet
是可被繼承的,可以通過 Rabbit.compare
和 Rabbit.planet
來訪問:
class Animal { static planet = "Earth"; constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // 繼承于 Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5. alert(Rabbit.planet); // Earth
現在我們調用 Rabbit.compare
時,繼承的 Animal.compare
將會被調用。
它是如何工作的?再次,使用原型。妳可能已經猜到了,extends
讓 Rabbit
的 [[Prototype]]
指向了 Animal
。
所以,Rabbit extends Animal
創建了兩個 [[Prototype]]
引用:
Rabbit
函數原型繼承自 Animal
函數。
Rabbit.prototype
原型繼承自 Animal.prototype
。
結果就是,繼承對常規方法和靜態方法都有效。
這裏,讓我們通過代碼來檢驗壹下:
class Animal {} class Rabbit extends Animal {} // 對于靜態的 alert(Rabbit.__proto__ === Animal); // true // 對于常規方法 alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
靜態方法被用于實現屬于整個類的功能。它與具體的類實例無關。
舉個例子, 壹個用于進行比較的方法 Article.compare(article1, article2)
或壹個工廠(factory)方法 Article.createTodays()
。
在類聲明中,它們都被用關鍵字 static
進行了標記。
靜態屬性被用于當我們想要存儲類級別的數據時,而不是綁定到實例。
語法如下所示:
class MyClass { static property = ...; static method() { ... } }
從技術上講,靜態聲明與直接給類本身賦值相同:
MyClass.property = ... MyClass.method = ...
靜態屬性和方法是可被繼承的。
對于 class B extends A
,類 B
的 prototype 指向了 A
:B.[[Prototype]] = A
。因此,如果壹個字段在 B
中沒有找到,會繼續在 A
中查找。
重要程度: 3
正如我們所知道的,所有的對象通常都繼承自 Object.prototype
,並且可以訪問“通用”對象方法,例如 hasOwnProperty
等。
例如:
class Rabbit { constructor(name) { this.name = name; } } let rabbit = new Rabbit("Rab"); // hasOwnProperty 方法來自于 Object.prototype alert( rabbit.hasOwnProperty('name') ); // true
但是,如果我們像這樣 "class Rabbit extends Object"
把它明確地寫出來,那麽結果會與簡單的 "class Rabbit"
有所不同麽?
不同之處在哪裏?
下面是此類的示例代碼(它無法正常運行 —— 爲什麽?修複它?):
class Rabbit extends Object { constructor(name) { this.name = name; } } let rabbit = new Rabbit("Rab"); alert( rabbit.hasOwnProperty('name') ); // Error
首先,讓我們看看爲什麽之前的代碼無法運行。
如果我們嘗試運行它,就會發現原因其實很明顯。派生類的 constructor 必須調用 super()
。否則 "this"
不會被定義。
下面是修複後的代碼:
class Rabbit extends Object { constructor(name) { super(); // 需要在繼承時調用父類的 constructor this.name = name; } } let rabbit = new Rabbit("Rab"); alert( rabbit.hasOwnProperty('name') ); // true
但這還不是全部原因。
即便修複了它,"class Rabbit extends Object"
和 class Rabbit
之間仍存在著壹個重要的差異。
我們知道,“extends” 語法會設置兩個原型:
在構造函數的 "prototype"
之間設置原型(爲了獲取實例方法)。
在構造函數之間會設置原型(爲了獲取靜態方法)。
在 class Rabbit extends Object
的例子中,意味著:
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 ); // true,所有函數都是默認如此 // error,Rabbit 中沒有這樣的函數 alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error
所以,在這種情況下,Rabbit
沒有提供對 Object
的靜態方法的訪問。
順便說壹下,Function.prototype
也有壹些“通用”函數方法,例如 call
和 bind
等。在上述的兩種情況下它們都是可用的,因爲對于內建的 Object
構造函數而言,Object.__proto__ === Function.prototype
。
我們用壹張圖來解釋:
所以,簡而言之,這裏有兩點區別:
class Rabbit | class Rabbit extends Object |
---|---|
– | 需要在 constructor 中調用 super() |
Rabbit.__proto__ === Function.prototype | Rabbit.__proto__ === Object |