你需要知道的 JavaScript 類(class)的這些知識

木子莫 發佈 2019-12-28T06:56:24+00:00

作者: Dmitri Pavlutin譯者:前端小智來源:dmitripavlutinJavaScript 使用原型繼承:每個對象都從原型對象繼承屬性和方法。


作者: Dmitri Pavlutin

譯者:前端小智

來源:dmitripavlutin

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.欄位

類欄位是保存信息的變量,欄位可以附加到兩個實體:

  1. 類實例上的欄位
  2. 類本身的欄位(也稱為靜態欄位)

欄位有兩種級別可訪問性:

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() { ... } 。

使用靜態方法時,有兩個簡單的規則需要記住:

  1. 靜態方法可以訪問靜態欄位。
  2. 靜態方法不能訪問實例欄位。

例如,創建一個靜態方法來檢測是否已經使用了具有特定名稱的用戶。

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 。


關鍵字: