一篇帶給你JavaScript的Class語法介紹
- 在面向對象的編程中,class 是用于創建對象的可擴展的程序代碼模版,它為對象提供了狀態(成員變量)的初始值和行為(成員函數或方法)的實現。
- Wikipedia
在日常開發中,我們經常需要創建許多相同類型的對象,例如用戶(users)、商品(goods)或者任何其他東西。
正如我們在 構造器和操作符 "new" 一章中已經學到的,new function 可以幫助我們實現這種需求。
但在現代 JavaScript 中,還有一個更高級的“類(class)”構造方式,它引入許多非常棒的新功能,這些功能對于面向對象編程很有用。
一、“class” 語法
基本語法是:
- class MyClass {
- // class 方法
- constructor() { ... }
- method1() { ... }
- method2() { ... }
- method3() { ... }
- ...
- }
然后使用 new MyClass() 來創建具有上述列出的所有方法的新對象。
new 會自動調用 constructor() 方法,因此我們可以在 constructor() 中初始化對象。
例如:
- class User {
- constructor(name) {
- this.name = name;
- }
- sayHi() {
- alert(this.name);
- }
- }
- // 用法:
- let user = new User("John");
- user.sayHi();
當 new User("John") 被調用:
- 一個新對象被創建。
- constructor 使用給定的參數運行,并為其分配 this.name。
……然后我們就可以調用對象方法了,例如 user.sayHi。
類的方法之間沒有逗號
對于新手開發人員來說,常見的陷阱是在類的方法之間放置逗號,這會導致語法錯誤。
不要把這里的符號與對象字面量相混淆。在類中,不需要逗號。
二、什么是 class?
所以,class 到底是什么?正如人們可能認為的那樣,這不是一個全新的語言級實體。
讓我們揭開其神秘面紗,看看類究竟是什么。這將有助于我們理解許多復雜的方面。
在 JavaScript 中,類是一種函數。
看看下面這段代碼:
- class User {
- constructor(name) { this.name = name; }
- sayHi() { alert(this.name); }
- }
- // 佐證:User 是一個函數
- alert(typeof User); // function
class User {...} 構造實際上做了如下的事兒:
- 創建一個名為 User 的函數,該函數成為類聲明的結果。該函數的代碼來自于 constructor 方法(如果我們不編寫這種方法,那么它就被假定為空)。
- 存儲類中的方法,例如 User.prototype 中的 sayHi。
當 new User 對象被創建后,當我們調用其方法時,它會從原型中獲取對應的方法,正如我們在 F.prototype 一章中所講的那樣。因此,對象 new User 可以訪問類中的方法。
我們可以將 class User 聲明的結果解釋為:
下面這些代碼很好地解釋了它們:
- class User {
- constructor(name) { this.name = name; }
- sayHi() { alert(this.name); }
- }
- // class 是一個函數
- alert(typeof User); // function
- // ...或者,更確切地說,是 constructor 方法
- alert(User === User.prototype.constructor); // true
- // 方法在 User.prototype 中,例如:
- alert(User.prototype.sayHi); // alert(this.name);
- // 在原型中實際上有兩個方法
- alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
三、不僅僅是語法糖
人們常說 class 是一個語法糖(旨在使內容更易閱讀,但不引入任何新內容的語法),因為我們實際上可以在沒有 class 的情況下聲明相同的內容:
- // 用純函數重寫 class User
- // 1. 創建構造器函數
- function User(name) {
- this.name = name;
- }
- // 函數的原型(prototype)默認具有 "constructor" 屬性,
- // 所以,我們不需要創建它
- // 2. 將方法添加到原型
- User.prototype.sayHi = function() {
- alert(this.name);
- };
- // 用法:
- let user = new User("John");
- user.sayHi();
這個定義的結果與使用類得到的結果基本相同。因此,這確實是將 class 視為一種定義構造器及其原型方法的語法糖的理由。
盡管,它們之間存在著重大差異:
- 首先,通過 class 創建的函數具有特殊的內部屬性標記 [[FunctionKind]]:"classConstructor"。因此,它與手動創建并不完全相同。編程語言會在許多地方檢查該屬性。例如,與普通函數不同,必須使用 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 方法,我們通常不希望 class 方法出現。
- 類總是使用 use strict。 在類構造中的所有代碼都將自動進入嚴格模式。
此外,class 語法還帶來了許多其他功能,我們稍后將會探索它們。
四、類表達式
就像函數一樣,類可以在另外一個表達式中被定義,被傳遞,被返回,被賦值等。
這是一個類表達式的例子:
- let User = class {
- sayHi() {
- alert("Hello");
- }
- };
類似于命名函數表達式(Named Function Expressions),類表達式可能也應該有一個名字。
如果類表達式有名字,那么該名字僅在類內部可見:
- // “命名類表達式(Named Class Expression)”
- // (規范中沒有這樣的術語,但是它和命名函數表達式類似)
- let User = class MyClass {
- sayHi() {
- alert(MyClass); // MyClass 這個名字僅在類內部可見
- }
- };
- new User().sayHi(); // 正常運行,顯示 MyClass 中定義的內容
- alert(MyClass); // error,MyClass 在外部不可見
我們甚至可以動態地“按需”創建類,就像這樣:
- function makeClass(phrase) {
- // 聲明一個類并返回它
- return class {
- sayHi() {
- alert(phrase);
- }
- };
- }
- // 創建一個新的類
- let User = makeClass("Hello");
- new User().sayHi(); // Hello
五、Getters/setters
就像對象字面量,類可能包括 getters/setters,計算屬性(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 is too short.
從技術上來講,這樣的類聲明可以通過在 User.prototype 中創建 getters 和 setters 來實現。
六、計算屬性名稱 […]
這里有一個使用中括號 [...] 的計算方法名稱示例:
- class User {
- ['say' + 'Hi']() {
- alert("Hello");
- }
- }
- new User().sayHi();
這種特性很容易記住,因為它們和對象字面量類似。
七、Class 字段
舊的瀏覽器可能需要 polyfill
類字段(field)是最近才添加到語言中的。
之前,我們的類僅具有方法。
“類字段”是一種允許添加任何屬性的語法。
例如,讓我們在 class User 中添加一個 name 屬性:
- 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)。
- 將方法綁定到對象,例如在 constructor 中。
類字段提供了另一種非常優雅的語法:
- 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; // 屬性
- constructor(...) { // 構造器
- // ...
- }
- method(...) {} // method
- get something(...) {} // getter 方法
- set something(...) {} // setter 方法
- [Symbol.iterator]() {} // 有計算名稱(computed name)的方法(此處為 symbol)
- // ...
- }
技術上來說,MyClass 是一個函數(我們提供作為 constructor 的那個),而 methods、getters 和 settors 都被寫入了 MyClass.prototype。