在面向對象的編程中,class 是用于創建對象的可擴展的程序代碼模版,它爲對象提供了狀態(成員變量)的初始值和行爲(成員函數或方法)的實現。
在日常開發中,我們經常需要創建許多相同類型的對象,例如用戶(users)、商品(goods)或者任何其他東西。
正如我們在 構造器和操作符 "new" 壹章中已經學到的,new function
可以幫助我們實現這種需求。
但在現代 JavaScript 中,還有壹個更高級的“類(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
到底是什麽?正如人們可能認爲的那樣,這不是壹個全新的語言級實體。
讓我們揭開其神秘面紗,看看類究竟是什麽。這將有助于我們理解許多複雜的方面。
在 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); // sayHi 方法的代碼 // 在原型中實際上有兩個方法 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
創建的函數具有特殊的內部屬性標記 [[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
方法,我們通常不希望 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,計算屬性(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();
這種特性很容易記住,因爲它們和對象字面量類似。
舊的浏覽器可能需要 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 和 setters 都被寫入了 MyClass.prototype
。
在下壹章,我們將會進壹步學習類的相關知識,包括繼承和其他功能。
重要程度: 5
Clock
類(請見沙箱)是以函數式編寫的。請以 “class” 語法重寫它。
P.S. 時鍾在控制台(console)中滴答,打開控制台即可查看。
打開壹個任務沙箱。
class Clock { constructor({ template }) { this.template = template; } render() { let date = new Date(); let hours = date.getHours(); if (hours < 10) hours = '0' + hours; let mins = date.getMinutes(); if (mins < 10) mins = '0' + mins; let secs = date.getSeconds(); if (secs < 10) secs = '0' + secs; let output = this.template .replace('h', hours) .replace('m', mins) .replace('s', secs); console.log(output); } stop() { clearInterval(this.timer); } start() { this.render(); this.timer = setInterval(() => this.render(), 1000); } } let clock = new Clock({template: 'h:m:s'}); clock.start();
使用沙箱打開解決方案。