做者:Dmitri Pavlutin翻譯:瘋狂的技術宅javascript
原文:https://dmitripavlutin.com/ja...前端
未經容許嚴禁轉載java
JavaScript 使用原型繼承:每一個對象都從其原型對象繼承屬性和方法。git
在 JavaScript 中不存在 Java 或 Swift 等語言中所使用的做爲建立對象 藍圖的傳統類,原型繼承僅處理對象。程序員
原型繼承能夠模仿經典類的繼承。爲了將傳統類引入 JavaScript,ES2015 標準引入了 class
語法:基於原型繼承上的語法糖。github
本文使你熟悉 JavaScript 類:如何定義類,初始化實例,定義字段和方法,瞭解私有字段和公共字段,掌握靜態字段和方法。面試
用特殊關鍵字 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) 時,該類將變得頗有用。實例是一個包含類描述的數據和行爲的對象。
new
運算符可在 JavaScript 中實例化該類:instance = new Class()
。
例如,你能夠用 new
運算符實例化 User
類:
const myUser = new User();
new User()
建立 User
類的實例。
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 類最多能夠有一個構造函數。
類字段是用來保存信息的變量。字段能夠附加到 2 個實體:
這些字段還具備 2 級可訪問性:
讓咱們再次看一下以前的代碼片斷:
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'
初始化它。
對公有字段的訪問或更新沒有任何限制。你能夠讀取它們的值並將其分配給構造函數、方法內部以及類外部的公有字段。
封裝是一個重要的概念,可以讓你隱藏類的內部細節。使用封裝類的人僅涉及該類提供的公共接口,而不會耦合到該類的實現細節。
當實現細節被更改時,考慮封裝性的類更易於更新。
使用私有字段是隱藏對象內部數據的一種好方法。這是隻能在它們所屬的類中讀取和修改的字段。該類的外部不能直接更改私有字段。
私有字段 僅可在類的正文中訪問。
在字段名前加上特殊符號 #
使其私有,例如 #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
。
你還能夠在類自己上定義字段:靜態字段 。它有助於定義類常量或存儲特定於類的信息。
要在 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
靜態 TYPE_ADMIN
和靜態 TYPE_REGULAR
定義了 User
類中的靜態變量。要訪問靜態字段,你必須使用類,後面跟字段名稱:User.TYPE_ADMIN和User.TYPE_REGULAR
。
有時甚至靜態字段也是你要隱藏的實現細節。在這方面,你能夠將靜態字段設爲私有。
要使靜態字段成爲私有字段,請在字段名稱前添加特殊符號 #
: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
類中訪問。外部世界都不會幹其擾限制機制:這就是封裝的好處。
這些字段用了保存數據。可是修改數據的能力是由屬於類的特殊函數執行的:方法。
JavaScript 類支持實例方法和靜態方法。
實例方法能夠訪問和修改實例數據。實例方法能夠調用其餘實例方法以及任何靜態方法。
例如,讓咱們定義一個方法 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()
。
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('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 將引起錯誤。
靜態方法是直接附加到類的函數。它們具備與類相關的邏輯,而不是與類的實例相關的邏輯。
要建立靜態方法,請使用特殊關鍵字 static
,後跟常規方法語法:static myStaticMethod() { ... }
。
使用靜態方法時,要記住兩個簡單的規則:
讓咱們建立一個靜態方法來檢測是否已經使用了具備特定名稱的 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(){...}
。它們一樣遵循私有規則:只能在類主體中調用私有靜態方法。
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; // => []
ContentWriter
從 User
繼承構造函數,getName()
方法和 name
字段。一樣,ContentWriter
類聲明一個新字段 posts
。
注意,父類的私有成員不會被子類所繼承。
若是你想在子類中調用父構的造函數,則須要使用子構造函數中提供的特殊功能 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); } }
若是你想在子方法中訪問父方法,則可使用特殊的快捷方式 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
與靜態方法一塊兒使用,來訪問父級的靜態方法。
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
。
同時 ContentWriter
是 User
的子類。所以 writer instanceof User
也將評估爲 true
。
若是你想肯定實例確切的類怎麼辦?能夠用 constructor
屬性並直接與該類進行比較:
writer.constructor === ContentWriter; // => true writer.constructor === User; // => false
我必須說,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 中使用類語法,我也建議你對原型繼承有所瞭解。
本文中介紹的課程功能涉及 ES2015 和第 3 階段的提案。
在2019年末,class 功能分爲如下兩部分:
JavaScript 類用構造函數初始化實例,定義字段和方法。你甚至可使用 static
關鍵字在類自己上附加字段和方法。
繼承是使用 extends
關鍵字實現的:你能夠輕鬆地從父級建立子級。super
關鍵字用於從子類訪問父類。
要使用封裝,請將字段和方法設爲私有來隱藏類的內部細節。私有字段和方法名稱必須以 #
開頭。
JavaScript 中的類正在變得愈來愈易於使用。