JavaScript 類完整指南

做者:Dmitri Pavlutin

翻譯:瘋狂的技術宅javascript

原文:https://dmitripavlutin.com/ja...前端

未經容許嚴禁轉載java

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

在 JavaScript 中不存在 Java 或 Swift 等語言中所使用的做爲建立對象 藍圖的傳統類,原型繼承僅處理對象。程序員

原型繼承能夠模仿經典類的繼承。爲了將傳統類引入 JavaScript,ES2015 標準引入了 class 語法:基於原型繼承上的語法糖。github

本文使你熟悉 JavaScript 類:如何定義類,初始化實例,定義字段和方法,瞭解私有字段和公共字段,掌握靜態字段和方法。面試

1.定義:class 關鍵字

用特殊關鍵字 class 在 JavaScript 中定義一個類:segmentfault

class User {
  // The body of class
}

上面的代碼定義了一個類 User。大括號 { } 界定了類的主體。請注意,此語法稱爲 類聲明服務器

你沒有義務指明 class 的名稱。經過使用類表達式 ,你能夠將類分配給變量:微信

const UserClass = class {
  // The body of class
};

能夠輕鬆地將類導出爲 ES2015 模塊的一部分。這是 默認導出 的語法:

export default class User {
 // The body of class
}

還有一個 命名導出

export class User {
  // The body of class
}

當你建立類的 實例(instance) 時,該類將變得頗有用。實例是一個包含類描述的數據和行爲的對象。

image.png

new 運算符可在 JavaScript 中實例化該類: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; // => 'Jon Snow'    this.name = name;
  }
}

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

構造函數中的 name 參數的值爲 'Jon Snow'

若是你沒有爲該類定義構造函數,則會建立一個默認的構造函數。默認構造函數是一個空函數,它不會修改實例。

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

3. 字段

類字段是用來保存信息的變量。字段能夠附加到 2 個實體:

  1. 類實例上的字段
  2. 類自己的字段(又稱爲靜態)

這些字段還具備 2 級可訪問性:

  1. 公共(public):該字段可在任何地方訪問
  2. 私有(private):只能在課程正文中訪問該字段

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 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'

類中的 name; 聲明瞭一個公共字段 name

以這種方式聲明的公共字段有很好的表現力:經過查看字段聲明就可以瞭解該類的數據結構。

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

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

const user = new User();
user.name; // => 'Unknown'

類中的 name ='Unknown' 聲明一個字段 name 並用值 'Unknown' 初始化它。

對公有字段的訪問或更新沒有任何限制。你能夠讀取它們的值並將其分配給構造函數、方法內部以及類外部的公有字段。

3.2 私有實例字段

封裝是一個重要的概念,可以讓你隱藏類的內部細節。使用封裝類的人僅涉及該類提供的公共接口,而不會耦合到該類的實現細節。

當實現細節被更改時,考慮封裝性的類更易於更新。

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

私有字段 僅可在類的正文中訪問。

在字段名前加上特殊符號 # 使其私有,例如 #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 is thrown

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

若是嘗試在用戶類主體以外訪問私有字段 #name,則會引起語法錯誤:SyntaxError: Private field '#name' must be declared in an enclosing class

3.3 公共靜態字段

你還能夠在類自己上定義字段:靜態字段 。它有助於定義類常量或存儲特定於類的信息。

要在 JavaScript 類中建立靜態字段,請使用特殊關鍵字 static ,後跟字段名稱:static myStaticField

讓咱們添加一個新的字段 type 來指示用戶類型:admin 或 Regular。靜態字段 TYPE_ADMINTYPE_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

靜態 TYPE_ADMIN 和靜態 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 實例方法

實例方法能夠訪問和修改實例數據。實例方法能夠調用其餘實例方法以及任何靜態方法。

例如,讓咱們定義一個方法 getName() ,該方法返回 User 類中的名稱:

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()

讓咱們添加一個新方法 name Contains(string),該方法有一個參數並調用另外一個方法:

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() 的另外一種方法來獲取用戶名。

方法也能夠是私有的。能夠經過前綴使方法私有,其名稱以#開頭。

讓咱們將 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()

做爲私有變量,不能在 User 類主體以外調用 #getName()

4.2 Getter 和 Setter

getter 和 setter 模仿常規字段,可是對如何訪問和修改字段有更多控制。

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

爲了確保 Username 屬性不能爲空,讓咱們將私有字段 #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('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

當你訪問字段 user.name 的值時,將執行 get name() {...} getter。

set name(name){...} 字段 user.name ='Jon White' 更新時執行。若是新值是一個空字符串,則 setter 將引起錯誤。

4.3靜態方法

靜態方法是直接附加到類的函數。它們具備與類相關的邏輯,而不是與類的實例相關的邏輯。

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

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

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

讓咱們建立一個靜態方法來檢測是否已經使用了具備特定名稱的 User。

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 從父類繼承構造函數字段和方法。

例如,讓咱們建立一個新的子類 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;     // => []

ContentWriterUser 繼承構造函數,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) {
    // Does not work!
    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'

子類 ContentWritergetName() 直接從父類 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

userUser 類的實例, user instanceof User 的計算結果爲 true

空對象 {} 不是 User 的實例,對應的 obj instanceof Userfalse

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 屬性並直接與該類進行比較:

writer.constructor === ContentWriter; // => true
writer.constructor === User;          // => false

7. 類和原型

我必須說,JavaScript 中的類語法在從原型繼承中進行抽象方面作得很好。爲了描述 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

使用原型的版本:

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 語言的經典繼承機制,則能夠更輕鬆地使用類語法。

無論怎樣,即使是你在 JavaScript 中使用類語法,我也建議你對原型繼承有所瞭解。

8. 類功能的可用性

本文中介紹的課程功能涉及 ES2015 和第 3 階段的提案。

在2019年末,class 功能分爲如下兩部分:

9. 結論

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

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

要使用封裝,請將字段和方法設爲私有來隱藏類的內部細節。私有字段和方法名稱必須以 # 開頭。

JavaScript 中的類正在變得愈來愈易於​​使用。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索