JavaScript Class 徹底指南

JavaScript 使用原型繼承:每一個對象都從原型對象繼承屬性和方法。javascript

Java 或 Swift 等語言中做爲建立對象的藍圖的傳統 Class,在JavaScript 中不存在。原型繼承只處理對象。前端

原型繼承能夠模擬經典的類繼承。爲了將傳統的類引入 JavaScript,ES2015 標準引入了class語法:這是在原型繼承之上的一種語法糖。java

這篇文章讓你熟悉 JavaScript 類:如何定義類,初始化實例,定義字段和方法,理解私有和公共字段,掌握靜態字段和方法。git

1. 定義: class 關鍵字

JavaScript 關鍵字class 用於定義類:github

class User {
  // The body of class
}
複製代碼

上面的代碼定義了一個類 User。花括號{ }標記 class 主體。注意,這個語法叫作 class 聲明bash

能夠不指定類名,你能夠經過使用 class 表達式 把 class 分配給一個變量:微信

const UserClass = class {
  // The body of class
};
複製代碼

您能夠輕鬆地將 class 導出爲 ES2015 模塊的一部分。下面是一個「默認導出」的語法:數據結構

export default class User {
 // The body of class
}
複製代碼

具名導出:ide

export class User {
  // The body of class
}
複製代碼

當你建立一個實例時,class 就變得很是有用。實例就是包含了 class 所描述的數據和行爲的一個對象。 函數

JavaScript 的new 操做符用於實例化 class :instance = new Class()

例如,你能夠用new 操做符實例化User 類:

const myUser = new User();
複製代碼

new User() 建立了 User 類的一個實例。

2. 構造函數: constructor()

constructor(param1, param2, ...) 是在 class 內部初始化實例的一個特殊方法。這是設置字段初始值或進行對象設置的地方。

下面的例子就是在構造函數裏設置name字段的初始值:

class User {
  constructor(name) {    this.name = name;  }}
複製代碼

User的構造函數有一個參數name,用於設置字段this.name的初始值。

構造函數內部的 this 值等於新建立的實例。

用來實例化類的參數變成了構造函數的參數:

class User {
  constructor(name) {
    name; // => 'Jon Snow'    this.name = name;
  }
}

const user = new User('Jon Snow');
複製代碼

構造函數內部的name 參數的值是'Jon Snow'

若是不定義類的構造函數,就會建立默認構造函數。默認構造函數是一個空函數,不會修改實例。

同時,JavaScript 類最多隻能有一個構造函數。

3. 字段

類字段是保存信息的變量。字段能夠附屬於兩種實體:

  1. class 實例字段
  2. class 自有字段(即靜態字段)

字段有兩種級別的可訪問性:

  1. 公有:字段可任意訪問
  2. 私有:只能在 class 內部訪問

3.1 公有實例字段

讓咱們看看以前的代碼:

class User {
  constructor(name) {
    this.name = name;  }
}
複製代碼

表達式this.name = name建立了一個實例字段name並設置了初始值。

以後就能夠經過屬性的形式訪問 name 字段:

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'
複製代碼

name是一個公有字段,由於你能夠在User 類外部訪問到它。

當字段在構造函數中隱式建立時,就像前面的例子同樣,可能很難管理字段列表。你必須從構造函數的代碼中破譯它們。

更好的方式是顯式地聲明 class 字段。不管構造函數作什麼,實例老是具備相同的字段列表。

class 字段提案 容許你在 class 主體中定義字段。另外,你能夠當即指定初始值:

class SomeClass {
  field1;  
  field2 = 'Initial value';
  // ...
}
複製代碼

讓咱們修改 User 類,聲明一個公有字段name

class User {
  name;  
  constructor(name) {
    this.name = name;
  }
}

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'
複製代碼

class 主體裏的name;聲明瞭一個公有字段name

以這種方式聲明的公共字段頗有表現力:快速查看字段聲明就足以知曉類的數據結構。

並且,類字段能夠在聲明時當即初始化。

class User {
  name = 'Unknown';
  constructor() {
    // No initialization
  }
}

const user = new User();
user.name; // => 'Unknown'
複製代碼

class 主體內的 name = 'Unknown' 聲明瞭一個 name 字段並設置了初始值'Unknown'

對公有字段的訪問和更新沒有限制。能夠在構造函數、方法以及 class 外部讀取和賦值給公有字段。

3.2 私有實例字段

封裝是一個重要的概念,它能夠隱藏 class 內部的細節。使用封裝類的人只依賴類提供的公共接口,而不與類的實現細節耦合。

組織 class 的時候充分考慮封裝,當實現細節改變的時候更新起來更容易。

隱藏對象內部數據的一種好方法是使用私有字段。這些字段只能在它們所屬的類中讀取和更改。類的外部不能直接更改私有字段。

私有字段 只能在 class 內部訪問。

在字段名前面加上特殊字符 # 可使其變爲私有,好比#myField。每次使用該字段時,前綴# 必須保留:聲明時、讀取時和修改時。

讓咱們確保字段 #name能夠在實例初始化時設置一次:

class User {
  #name;
  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'

user.#name; // 拋出 SyntaxError 異常
複製代碼

#name 是私有字段。你能夠在 User內部訪問和修改 #name。 getName() 方法能夠訪問私有字段 #name

可是若是你嘗試從 User 類外部訪問私有變量#name,就會拋出語法錯誤:SyntaxError: Private field '#name' must be declared in an enclosing class

3.3 公有靜態字段

你也能夠在 class 本身上面定義字段:靜態字段。這有助於定義類常量或存儲特定於該類的信息。

要在 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('Site Admin', 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('Jon Snow');
new User('Arya Stark');
new User('Sansa Stark'); // throws Error
複製代碼

靜態字段 User.#MAX_INSTANCES設置了容許的最大實例數量,靜態字段User.#instances是實際建立的實例數量。

私有靜態字段只能在 User 類內部訪問。外部範圍沒法干預這裏的限制機制:這就是封裝的好處。

4. 方法

字段包含了數據。可是修改數據的能力是由特殊函數提供的,它是類的一部分:方法

JavaScript 類支持實例方法和靜態方法。

4.1 實例方法

實例方法能夠訪問和修改實例數據。實例方法能夠調用其餘實例方法,也能夠調用任意靜態方法。

例如,咱們在 User 類中定義一個 getName() 方法,用來返回 name

class User {
  name = 'Unknown';

  constructor(name) {
    this.name = name;
  }

  getName() {    return this.name;  }}

const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'
複製代碼

getName() { ... } 是User 類中的一個方法。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('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => 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('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

user.#getName(); // SyntaxError is thrown
複製代碼

#getName()是個私有方法。在方法nameContains(str)內部,用這種方式調用私有方法:this.#getName()

因爲是私有的,#getName()不能在User 類外部被調用。

4.2 getters 和 setters

getter 和 setter 模擬常規字段,但對如何訪問和更改字段有更多的控制。

getter 在試圖獲取字段值時執行,而 setter 在試圖設置值時執行。

爲了確保 User 的 name屬性不爲空,讓咱們在 getter 和 setter 中包裝私有字段#nameValue :

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('Jon Snow');
user.name; // The getter is invoked, => 'Jon Snow'
user.name = 'Jon White'; // The setter is invoked

user.name = ''; // The setter throws an Error
複製代碼

get name() {...} getter 在你訪問字段 user.name時執行。

set name(name) {...}  在字段更新user.name = 'Jon White'時執行。若是新的值是空字符串,setter 就會拋出錯誤。

4.3 靜態方法

靜態方法是直接附屬於類的方法。它們包含了跟類相關的邏輯,而不是類的實例。

要建立靜態方法,請使用特殊的關鍵字static ,後面加上常規的方法語法:static myStaticMethod() { ... }

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

  1. 靜態方法能夠訪問靜態字段
  2. 靜態方法不能訪問實例字段

例如,咱們來建立一個靜態方法,用於檢測某個用戶名是否被佔用。

class User {
  static #takenNames = [];

  static isNameTaken(name) {   
    return User.#takenNames.includes(name); 
  }
   name = 'Unknown';

   constructor(name) {
    this.name = name;
    User.#takenNames.push(name);
  }
}

const user = new User('Jon Snow');

User.isNameTaken('Jon Snow');   // => true
User.isNameTaken('Arya Stark'); // => false
複製代碼

isNameTaken()是個靜態方法,使用了靜態私有字段User.#takenNames 檢查被佔用的名字。

靜態方法能夠是私有的: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('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
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 'Unknwon';
    }
    return name;
  }
}

const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknwon'
複製代碼

子類 ContentWriter 中的getName()訪問了父類 User的方法 super.getName()

該特性叫作方法重寫.

注意,你也能夠在靜態方法中使用 super ,用於訪問父類的靜態方法。

6. 對象類型檢測:instanceof

object instanceof Class 是用來判斷object 是否爲 Class實例的操做符。

咱們來看看instanceof實際是怎麼用的:

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('Jon Snow');
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('John Smith', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User;          // => true
複製代碼

writer是子類 ContentWriter的實例。操做符 writer instanceof ContentWriter是值爲 true

同時,ContentWriterUser的子類,所以 writer instanceof User 也是true

若是要判斷實例的確切類要怎麼作?你可使用 constructor屬性,並與 class 直接比較:

writer.constructor === ContentWriter; // => true
writer.constructor === User;          // => false
複製代碼

7. 類與原型

必須這樣說,JavaScript 的 class 語法很好地抽象了原型繼承機制。爲了描述 class語法,我甚至沒用到「prototype」這個詞。

可是 class 是在原型繼承的基礎上構建的。每一個類都是一個函數,並在做爲構造函數調用時建立一個實例。

下面這兩段代碼是等效的。

class 版本:

class User {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('John');

user.getName();       // => 'John Snow'
user instanceof User; // => true
複製代碼

prototype 版本:

function User(name) {
  this.name = name;
}

User.prototype.getName = function() {
  return this.name;
}

const user = new User('John');

user.getName();       // => 'John Snow'
user instanceof User; // => true
複製代碼

若是您熟悉 Java 或 Swift 語言的經典繼承機制,那麼 class 語法更容易使用。

無論怎麼樣,即便你在 JavaScript 中使用 class 語法,我仍是推薦你好好掌握原型繼承

8. class 特性可用性

本文提到的 class 特性出如今 ES2015 和 stage 3 提案。

到 2019 年末,class 特性分佈在如下幾個提案和標準中:

9. 總結

JavaScript 類用構造函數初始化實例、定義字段和方法。你甚至可使用static關鍵字在類上面附加字段和方法

繼承是經過 extends關鍵字實現的:你能夠輕鬆地從父類建立子類。super 關鍵字用於子類訪問父類。

爲了利用封裝,讓字段和方法變成私有以便隱藏 class 的內部細節。私有字段和方法名必須以#開頭。

JavaScript 中的類變得愈來愈方便使用了。

在私有屬性前加上# 前綴,你怎麼看?

做者:Dmitri Pavlutin
原文:dmitripavlutin.com/javascript-…
翻譯:1024譯站

更多前端技術乾貨盡在微信公衆號:1024譯站

微信公衆號:1024譯站
相關文章
相關標籤/搜索