在物件導向程式設計中,類別是可擴充的程式碼範本,用於建立物件,提供狀態 (成員變數) 的初始值和行為 (成員函式或方法) 的實作。
在實務上,我們經常需要建立很多種類似物件,例如使用者、商品或其他任何東西。
正如我們從章節 建構函式、運算子「new」 所知,new function
可以協助我們達成此目的。
但在現代的 JavaScript 中,有一個更進階的「類別」建構,它引入了許多對物件導向程式設計有用的新功能。
「類別」語法
基本語法為
class MyClass {
// class methods
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
然後使用 new MyClass()
來建立一個包含所有列出方法的新物件。
constructor()
方法會由 new
自動呼叫,因此我們可以在這裡初始化物件。
例如
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// Usage:
let user = new User("John");
user.sayHi();
當呼叫 new User("John")
時
- 會建立一個新的物件。
constructor
會執行指定的參數,並將其指定給this.name
。
…然後我們就可以呼叫物件方法,例如 user.sayHi()
。
初學開發人員常犯的錯誤是,在類別方法之間加上逗號,這會導致語法錯誤。
此處的表示法不能與物件文字混淆。在類別中,不需要逗號。
什麼是類別?
那麼,class
究竟是什麼?這並不是一個全新的語言層級實體,就像人們可能認為的那樣。
讓我們揭開任何魔法,看看類別的本質。這有助於理解許多複雜的方面。
在 JavaScript 中,類別是一種函式。
請看這裡
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// proof: User is a function
alert(typeof User); // function
class User {...}
結構實際上執行的動作是
- 建立一個名為
User
的函式,成為類別宣告的結果。函式程式碼取自constructor
方法(如果我們沒有撰寫此方法,則假設為空)。 - 將類別方法(例如
sayHi
)儲存在User.prototype
中。
建立 new User
物件後,當我們呼叫其方法時,它會從原型中取得,正如 F.prototype 章節中所述。因此,物件可以存取類別方法。
我們可以說明 class User
宣告的結果如下
以下是內省它的程式碼
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// class is a function
alert(typeof User); // function
// ...or, more precisely, the constructor method
alert(User === User.prototype.constructor); // true
// The methods are in User.prototype, e.g:
alert(User.prototype.sayHi); // the code of the sayHi method
// there are exactly two methods in the prototype
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
不只是語法糖
人們有時會說 class
是「語法糖」(一種旨在讓事物更易於閱讀的語法,但不會引入任何新事物),因為我們實際上可以在完全不使用 class
關鍵字的情況下宣告相同的事物
// rewriting class User in pure functions
// 1. Create constructor function
function User(name) {
this.name = name;
}
// a function prototype has "constructor" property by default,
// so we don't need to create it
// 2. Add the method to prototype
User.prototype.sayHi = function() {
alert(this.name);
};
// Usage:
let user = new User("John");
user.sayHi();
此定義的結果大致相同。因此,確實有理由將 class
視為語法糖,用於定義建構函式及其原型方法。
儘管如此,還是有重要的差異。
-
首先,由
class
建立的函式會標記一個特殊的內部屬性[[IsClassConstructor]]: true
。因此,它與手動建立並不完全相同。語言在各種地方檢查該屬性。例如,與一般函數不同,它必須使用
new
呼叫class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'
此外,在大部分 JavaScript 引擎中,類別建構函式的字串表示會以「class…」開頭
class User { constructor() {} } alert(User); // class User { ... }
還有其他差異,我們很快就會看到
-
類別方法不可列舉。類別定義會將
"prototype"
中所有方法的enumerable
旗標設為false
這很好,因為如果我們對物件執行
for..in
,通常不想要它的類別方法 -
類別總是
use strict
。類別建構中的所有程式碼都會自動進入嚴格模式
此外,class
語法帶來了許多其他功能,我們稍後會探討
類別表達式
就像函數一樣,類別可以在另一個表達式中定義,傳遞、傳回、指定等
以下是類別表達式的範例
let User = class {
sayHi() {
alert("Hello");
}
};
類似於命名函數表達式,類別表達式可以有名稱
如果類別表達式有名稱,則只會在類別內部可見
// "Named Class Expression"
// (no such term in the spec, but that's similar to Named Function Expression)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass name is visible only inside the class
}
};
new User().sayHi(); // works, shows MyClass definition
alert(MyClass); // error, MyClass name isn't visible outside of the class
我們甚至可以這樣動態地「依需求」建立類別
function makeClass(phrase) {
// declare a class and return it
return class {
sayHi() {
alert(phrase);
}
};
}
// Create a new class
let User = makeClass("Hello");
new User().sayHi(); // Hello
Getter/setter
就像文字物件一樣,類別可以包含 getter/setter、運算屬性等
以下是使用 get/set
實作的 user.name
範例
class User {
constructor(name) {
// invokes the 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 is too short.
技術上來說,此類別宣告會在 User.prototype
中建立 getter 和 setter
運算名稱 […]
以下是使用方括號 [...]
的運算方法名稱範例
class User {
['say' + 'Hi']() {
alert("Hello");
}
}
new User().sayHi();
這些功能很容易記住,因為它們類似於文字物件
類別欄位
類別欄位是語言最近新增的功能
以前,我們的類別只有方法
「類別欄位」是一種語法,允許新增任何屬性
例如,我們將 name
屬性新增到 class User
class User {
name = "John";
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi(); // Hello, John!
因此,我們只要在宣告中寫入 "
類別欄位的重大差異在於,它們設定在個別物件上,而不是 User.prototype
class User {
name = "John";
}
let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
我們也可以使用更複雜的表達式和函數呼叫來指定值
class User {
name = prompt("Name, please?", "John");
}
let user = new User();
alert(user.name); // John
使用類別欄位建立繫結方法
如 函數繫結 章節中所示,JavaScript 中的函數有一個動態 this
。它取決於呼叫的內容
因此,如果一個物件方法被傳遞並在另一個內容中呼叫,this
將不再是其物件的參考。
例如,此程式碼將顯示 undefined
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
這個問題稱為「失去 this
」。
有兩種方法可以修正它,如 函式繫結 章節中所討論的
- 傳遞一個包裝函式,例如
setTimeout(() => button.click(), 1000)
。 - 將方法繫結到物件,例如在建構函式中。
類別欄位提供了另一種相當簡潔的語法
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
類別欄位 click = () => {...}
是根據每個物件建立的,每個 Button
物件都有個別的函式,其中 this
參照該物件。我們可以在任何地方傳遞 button.click
,而 this
的值將永遠是正確的。
這在瀏覽器環境中特別有用,適用於事件監聽器。
摘要
基本的類別語法如下所示
class MyClass {
prop = value; // property
constructor(...) { // constructor
// ...
}
method(...) {} // method
get something(...) {} // getter method
set something(...) {} // setter method
[Symbol.iterator]() {} // method with computed name (symbol here)
// ...
}
MyClass
在技術上是一個函式(我們提供為 constructor
的函式),而方法、getter 和 setter 則寫入 MyClass.prototype
。
在後面的章節中,我們將進一步了解類別,包括繼承和其他功能。
留言
<code>
標籤,對於多行程式碼,請將它們包覆在<pre>
標籤中,對於超過 10 行的程式碼,請使用沙盒(plnkr、jsbin、codepen…)