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

做者: Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin
點贊再看,養成習慣

本文 GitHub https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。javascript


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

JavaSwift等語言中使用的傳統類做爲建立對象的藍圖,在 JavaScript 中不存在,原型繼承僅處理對象。java

原型繼承能夠模擬經典類繼承。爲了將傳統的類引入JavaScript, ES2015 標準引入了class語法,其底層實現仍是基於原型,只是原型繼承的語法糖。git

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

1. 定義:類關鍵字

使用關鍵字class能夠在 JS 中定義了一個類:面試

class User {
  // 類的主體
}

上面的代碼定義了一個User類。 大括號{}裏面是類的主體。 此語法稱爲class 聲明。segmentfault

若是在定義類時沒有指定類名。能夠經過使用類表達式,將類分配給變量:數據結構

const UserClass = class {
  // 類的主體
}

還能夠輕鬆地將類導出爲 ES6 模塊的一部分,默認導出語法以下:ide

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. 類自己的字段(也稱爲靜態字段)

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

  1. public:該字段能夠在任何地方訪問
  2. 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:adminregular。靜態字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('前端小智', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

static TYPE_ADMINstatic TYPE_REGULARUser類內部定義了靜態變量。 要訪問靜態字段,必須使用後跟字段名稱的類:User.TYPE_ADMINUser.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

gettersetter模仿常規字段,可是對如何訪問和更改字段具備更多控制。在嘗試獲取字段值時執行getter,而在嘗試設置值時使用setter

爲了確保Username屬性不能爲空,咱們將私有字段#nameValue封裝在gettersetter中:

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(); // => '無名氏'

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

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('前端小智', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User;          // => true

writer是子類ContentWriter的一個實例。運算符writer instanceof ContentWriter的計算結果爲true

同時ContentWriterUser的子類。所以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

若是你熟悉JavaSwift語言的經典繼承機制,則能夠更輕鬆地使用類語法。

8. 類的可用性

這篇文章中的類的一些特性有些還在分佈第三階段的提案中。在2019年末,類的特性分爲如下兩部分:

9. 總結

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

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

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

你對使用#前綴私有屬性有何見解,歡迎留言討論?


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

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug


交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索