作者: Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin
點贊再看,養成習慣本文
GitHub
https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了很多我的文檔,和教程資料。歡迎Star和完善,大家面試可以參照考點復習,希望我們一起有點東西。
JavaScript 使用原型繼承:每個對象都從原型對象繼承屬性和方法。
在Java
或Swift
等語言中使用的傳統類作為創建對象的藍圖,在 JavaScript 中不存在,原型繼承僅處理對象。
原型繼承可以模擬經典類繼承。為了將傳統的類引入JavaScript, ES2015 標准引入了class
語法,其底層實現還是基於原型,只是原型繼承的語法糖。
這篇文章主要讓你熟悉 JavaScript 類:如何定義類,初始化實例,定義字段和方法,理解私有和公共字段,掌握靜態字段和方法。
1. 定義:類關鍵字
使用關鍵字class
可以在 JS 中定義了一個類:
class User {
// 類的主體
}
上面的代碼定義了一個User
類。 大括號{}
里面是類的主體。 此語法稱為class
聲明。
如果在定義類時沒有指定類名。可以通過使用類表達式
,將類分配給變量:
const UserClass = class {
// 類的主體
}
還可以輕松地將類導出為 ES6 模塊的一部分,默認導出語法如下:
export default class User {
// 主體
}
命名導出如下:
export class User {
// 主體
}
當我們創建類的實例時,該類將變得非常有用。實例是包含類所描述的數據和行為的對象。
使用new
運算符實例化該類,語法:instance = new Class()
。
例如,可以使用new
操作符實例化User
類:
const myUser = new User();
new User()
創建User
類的一個實例。
2. 初始化:constructor()
constructor(param1, param2, ...)
是用於初始化實例的類主體中的一種特殊方法。 在這里可以設置字段的初始值或進行任何類型的對象設置。
在下面的示例中,構造函數設置字段name
的初始值
class User {
constructor(name) {
this.name = name;
}
}
User
的構造函數有一個參數 name
,用於設置字段this.name
的初始值
在構造函數中,this
值等於新創建的實例。用於實例化類的參數成為構造函數的參數:
class User {
constructor(name) {
name; // => '前端小智'
this.name = name;
}
}
const user = new User('前端小智');
構造函數中的name
參數的值為'前端小智'
。如果沒有定義該類的構造函數,則會創建一個默認的構造函數。默認的構造函數是一個空函數,它不修改實例。
同時,一個JavaScript 類最多可以有一個構造函數。
3.字段
類字段是保存信息的變量,字段可以附加到兩個實體:
- 類實例上的字段
- 類本身的字段(也稱為靜態字段)
字段有兩種級別可訪問性:
-
public
:該字段可以在任何地方訪問 -
private
:字段只能在類的主體中訪問
3.1 公共實例字段
讓我們再次看看前面的代碼片段:
class User {
constructor(name) {
this.name = name;
}
}
表達式this.name = name
創建一個實例字段名,並為其分配一個初始值。然后,可以使用屬性訪問器訪問name
字段
const user = new User('前端小智');
user.name; // => '前端小智'
name
是一個公共字段,因為你可以在User
類主體之外訪問它。
當字段在構造函數中隱式創建時,就像前面的場景一樣,可能獲取所有字段。必須從構造函數的代碼中破譯它們。
class fields proposal
提案允許我們在類的主體中定義字段,並且可以立即指定初始值:
class SomeClass {
field1;
field2 = 'Initial value';
// ...
}
接着我們修改User
類並聲明一個公共字段name
:
class User {
name;
constructor(name) {
this.name = name;
}
}
const user = new User('前端小智');
user.name; // => '前端小智'
name;
在類的主體中聲明一個公共字段name
。
以這種方式聲明的公共字段具有表現力:快速查看字段聲明就足以了解類的數據結構,而且,類字段可以在聲明時立即初始化。
class User {
name = '無名氏'
constructor () {
}
}
const user = new User();
user.name; // '無名氏'
類體內的name ='無名氏'
聲明一個字段名稱,並使用值'無名氏'
對其進行初始化。
對公共字段的訪問或更新沒有限制。可以讀取構造函數、方法和類外部的公共字段並將其賦值。
3.2 私有實例字段
封裝是一個重要的概念,它允許我們隱藏類的內部細節。使用封裝類只依賴類提供的公共接口,而不耦合類的實現細節。
當實現細節改變時,考慮到封裝而組織的類更容易更新。
隱藏對象內部數據的一種好方法是使用私有字段。這些字段只能在它們所屬的類中讀取和更改。類的外部世界不能直接更改私有字段。
私有字段只能在類的主體中訪問。
在字段名前面加上特殊的符號#
使其成為私有的,例如#myField
。每次處理字段時都必須保留前綴#
聲明它、讀取它或修改它。
確保在實例初始化時可以一次設置字段#name
:
class User {
#name;
constructor (name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const user = new User('前端小智')
user.getName() // => '前端小智'
user.#name // 拋出語法錯誤
#name
是一個私有字段。可以在User
內訪問和修改#name
。方法getName()
可以訪問私有字段#name
。
但是,如果我們試圖在 User
主體之外訪問私有字段#name
,則會拋出一個語法錯誤:SyntaxError: Private field '#name' must be declared in an enclosing class
。
3.3 公共靜態字段
我們還可以在類本身上定義字段:靜態字段
。這有助於定義類常量或存儲特定於該類的信息。
要在 JavaScript 類中創建靜態字段,請使用特殊的關鍵字static
后面跟字段名:static myStaticField
讓我們添加一個表示用戶類型的新字段type
:admin
或regular
。靜態字TYPE_ADMIN
和TYPE_REGULAR
是區分用戶類型的常量:
class User {
static TYPE_ADMIN = 'admin';
static TYPE_REGULAR = 'regular';
name;
type;
constructor(name, type) {
this.name = name;
this.type = type;
}
}
const admin = new User('前端小智', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true
static TYPE_ADMIN
和static TYPE_REGULAR
在User
類內部定義了靜態變量。 要訪問靜態字段,必須使用后跟字段名稱的類:User.TYPE_ADMIN
和User.TYPE_REGULAR
。
3.4 私有靜態字段
有時,我們也想隱藏靜態字段的實現細節,在時候,就可以將靜態字段設為私有。
要使靜態字段成為私有的,只要字段名前面加上#
符號:static #myPrivateStaticField
。
假設我們希望限制User
類的實例數量。要隱藏實例限制的詳細信息,可以創建私有靜態字段:
class User {
static #MAX_INSTANCES = 2;
static #instances = 0;
name;
constructor(name) {
User.#instances++;
if (User.#instances > User.#MAX_INSTANCES) {
throw new Error('Unable to create User instance');
}
this.name = name;
}
}
new User('張三');
new User('李四');
new User('王五'); // throws Error
靜態字段User.#MAX_INSTANCES
設置允許的最大實例數,而User.#instances
靜態字段則計算實際的實例數。
這些私有靜態字段只能在User
類中訪問,類的外部都不會干擾限制機制:這就是封裝的好處。
4.方法
字段保存數據,但是修改數據的能力是由屬於類的一部分的特殊功能實現的:方法。
JavaScript 類同時支持實例和靜態方法。
4.1 實例方法
實例方法可以訪問和修改實例數據。實例方法可以調用其他實例方法,也可以調用任何靜態方法。
例如,定義一個方法getName()
,它返回User
類中的name
:
class User {
name = '無名氏';
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('前端小智');
user.getName(); // => '前端小智'
getName() { ... }
是User
類中的一個方法,getname()
是一個方法調用:它執行方法並返回計算值(如果存在的話)。
在類方法和構造函數中,this
值等於類實例。使用this
來訪問實例數據:this.field
或者調用其他方法:this.method()
。
接着我們添加一個具有一個參數並調用另一種方法的新方法名稱nameContains(str)
:
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
nameContains(str) {
return this.getName().includes(str);
}
}
const user = new User('前端小智');
user.nameContains('前端'); // => true
user.nameContains('大冶'); // => false
nameContains(str) { ... }
是User
類的一種方法,它接受一個參數str
。 不僅如此,它還執行實例this.getName()
的方法來獲取用戶名。
方法也可以是私有的。 為了使方法私有前綴,名稱以#開頭即可,如下所示:
class User {
#name;
constructor(name) {
this.#name = name;
}
#getName() {
return this.#name;
}
nameContains(str) {
return this.#getName().includes(str);
}
}
const user = new User('前端小智');
user.nameContains('前端'); // => true
user.nameContains('大冶'); // => false
user.#getName(); // SyntaxError is thrown
#getName()
是一個私有方法。在方法nameContains(str)
中,可以這樣調用一個私有方法:this.#getName()
。
由於是私有的,#getName()
不能在用User
類主體之外調用。
4.2 getters 和 setters
getter
和setter
模仿常規字段,但是對如何訪問和更改字段具有更多控制。在嘗試獲取字段值時執行getter
,而在嘗試設置值時使用setter
。
為了確保User
的name
屬性不能為空,我們將私有字段#nameValue
封裝在getter
和setter
中:
class User {
#nameValue;
constructor(name) {
this.name = name;
}
get name() {
return this.#nameValue;
}
set name(name) {
if (name === '') {
throw new Error(`name field of User cannot be empty`);
}
this.#nameValue = name;
}
}
const user = new User('前端小智');
user.name; // getter 被調用, => '前端小智'
user.name = '王大冶'; // setter 被調用
user.name = ''; // setter 拋出一個錯誤
get name() {...}
在訪問user.name
會被執行。而set name(name){…}
在字段更新(user.name = '前端小智'
)時執行。如果新值是空字符串,setter
將拋出錯誤。
4.3 靜態方法
靜態方法是直接附加到類的函數,它們持有與類相關的邏輯,而不是類的實例。
要創建一個靜態方法,請使用特殊的關鍵字static
和一個常規的方法語法:static myStaticMethod() { ... }
。
使用靜態方法時,有兩個簡單的規則需要記住:
- 靜態方法可以訪問靜態字段。
- 靜態方法不能訪問實例字段。
例如,創建一個靜態方法來檢測是否已經使用了具有特定名稱的用戶。
class User {
static #takenNames = [];
static isNameTaken(name) {
return User.#takenNames.includes(name);
}
name = '無名氏';
constructor(name) {
this.name = name;
User.#takenNames.push(name);
}
}
const user = new User('前端小智');
User.isNameTaken('前端小智'); // => true
User.isNameTaken('王大冶'); // => false
isNameTaken()
是一個使用靜態私有字段User
的靜態方法用於檢查已取的名字。
靜態方法可以是私有的:static #staticFunction() {...}
。同樣,它們遵循私有規則:只能在類主體中調用私有靜態方法。
5. 繼承: extends
JavaScript 中的類使用extends
關鍵字支持單繼承。
在class Child extends Parent { }
表達式中,Child
類從Parent
繼承構造函數,字段和方法。
例如,我們創建一個新的子類ContentWriter
來繼承父類User
。
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
}
const writer = new ContentWriter('John Smith');
writer.name; // => 'John Smith'
writer.getName(); // => 'John Smith'
writer.posts; // => []
ContentWriter
繼承了User
的構造函數,方法getName()
和字段name
。同樣,ContentWriter
類聲明了一個新的字段posts
。
注意,父類的私有成員不會被子類繼承。
5.1 父構造函數:constructor()
中的super()
如果希望在子類中調用父構造函數,則需要使用子構造函數中可用的super()
特殊函數。
例如,讓ContentWriter
構造函數調用User
的父構造函數,以及初始化posts
字段
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
constructor(name, posts) {
super(name);
this.posts = posts;
}
}
const writer = new ContentWriter('前端小智', ['Why I like JS']);
writer.name; // => '前端小智'
writer.posts // => ['Why I like JS']
子類ContentWriter
中的super(name)
執行父類User
的構造函數。
注意,在使用this
關鍵字之前,必須在子構造函數中執行super()
。調用super()
確保父構造函數初始化實例。
class Child extends Parent {
constructor(value1, value2) {
//無法工作
this.prop2 = value2;
super(value1);
}
}
5.2 父實例:方法中的super
如果希望在子方法中訪問父方法,可以使用特殊的快捷方式super
。
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
constructor(name, posts) {
super(name);
this.posts = posts;
}
getName() {
const name = super.getName();
if (name === '') {
return '無名氏';
}
return name;
}
}
const writer = new ContentWriter('前端小智', ['Why I like JS']);
writer.getName(); // => '無名氏'
子類ContentWriter
的getName()
直接從父類User
訪問方法super.getName()
,這個特性稱為方法重寫
。
注意,也可以在靜態方法中使用super
來訪問父類的靜態方法。
6.對象類型檢查:instanceof
object instanceof Class
是確定object
是否為Class
實例的運算符,來看看示例:
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('前端小智');
const obj = {};
user instanceof User; // => true
obj instanceof User; // => false
user
是User
類的一個實例,user instanceof User
的計算結果為true
。
空對象{}
不是User
的實例,相應地obj instanceof User
為false
。
instanceof
是多態的:操作符檢測作為父類實例的子類。
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
constructor(name, posts) {
super(name);
this.posts = posts;
}
}
const writer = new ContentWriter('前端小智', ['Why I like JS']);
writer instanceof ContentWriter; // => true
writer instanceof User; // => true
writer
是子類ContentWriter
的一個實例。運算符writer instanceof ContentWriter
的計算結果為true
。
同時ContentWriter
是User
的子類。因此writer instanceof User
結果也為true
。
如果想確定實例的確切類,該怎么辦?可以使用構造函數屬性並直接與類進行比較
writer.constructor === ContentWriter; // => true
writer.constructor === User; // => false
7. 類和原型
必須說 JS 中的類語法在從原型繼承中抽象方面做得很好。但是,類是在原型繼承的基礎上構建的。每個類都是一個函數,並在作為構造函數調用時創建一個實例。
以下兩個代碼段是等價的。
類版本:
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('前端小智');
user.getName(); // => '前端小智'
user instanceof User; // => true
使用原型的版本:
function User(name) {
this.name = name;
}
User.prototype.getName = function() {
return this.name;
}
const user = new User('前端小智');
user.getName(); // => '前端小智'
user instanceof User; // => true
如果你熟悉Java
或Swift
語言的經典繼承機制,則可以更輕松地使用類語法。
8. 類的可用性
這篇文章中的類的一些特性有些還在分布第三階段的提案中。在2019
年底,類的特性分為以下兩部分:
- 公共和私有實例字段是Class fields proposal建議的一部分
- 私有實例方法和訪問器是Class private methods proposal建議的一部分
- 其余部分為ES6 標准的一部分。
9. 總結
JavaScript 類用構造函數初始化實例,定義字段和方法。甚至可以使用static
關鍵字在類本身上附加字段和方法。
繼承是使用extends
關鍵字實現的:可以輕松地從父類創建子類,super
關鍵字用於從子類訪問父類。
要利用封裝,將字段和方法設為私有以隱藏類的內部細節,私有字段和方法名必須以#
開頭。
你對使用#前
綴私有屬性有何看法,歡迎留言討論?
原文:https://dmitripavlutin.com/ja...
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流
干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后台回復福利,即可看到福利,你懂的。