JavaScript 使用原型繼承:每一個對象都從原型對象繼承屬性和方法。javascript
Java 或 Swift 等語言中做爲建立對象的藍圖的傳統 Class,在JavaScript 中不存在。原型繼承只處理對象。前端
原型繼承能夠模擬經典的類繼承。爲了將傳統的類引入 JavaScript,ES2015 標準引入了class
語法:這是在原型繼承之上的一種語法糖。java
這篇文章讓你熟悉 JavaScript 類:如何定義類,初始化實例,定義字段和方法,理解私有和公共字段,掌握靜態字段和方法。git
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
類的一個實例。
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 類最多隻能有一個構造函數。
類字段是保存信息的變量。字段能夠附屬於兩種實體:
字段有兩種級別的可訪問性:
讓咱們看看以前的代碼:
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 外部讀取和賦值給公有字段。
封裝是一個重要的概念,它能夠隱藏 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
。
你也能夠在 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
有時甚至靜態字段也是你但願隱藏的實現細節。在這裏,你也能夠將靜態字段設爲私有。
要將靜態字段設爲私有,只要在字段名前面加上特殊符號#
: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 類支持實例方法和靜態方法。
實例方法能夠訪問和修改實例數據。實例方法能夠調用其餘實例方法,也能夠調用任意靜態方法。
例如,咱們在 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
類外部被調用。
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 就會拋出錯誤。
靜態方法是直接附屬於類的方法。它們包含了跟類相關的邏輯,而不是類的實例。
要建立靜態方法,請使用特殊的關鍵字static
,後面加上常規的方法語法:static myStaticMethod() { ... }
使用靜態方法時,須要記住兩個簡單的規則:
例如,咱們來建立一個靜態方法,用於檢測某個用戶名是否被佔用。
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
類繼承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
。
注意,父類的私有成員不能被子類繼承。
若是你想在子類中調用父類的構造函數,你須要在子類構造函數中使用特殊的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); }
}
複製代碼
若是你想在子類方法中訪問父類方法,你可使用特殊的快捷方式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
屬性,並與 class 直接比較:
writer.constructor === ContentWriter; // => true
writer.constructor === User; // => false
複製代碼
必須這樣說,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 語法,我仍是推薦你好好掌握原型繼承
本文提到的 class 特性出如今 ES2015 和 stage 3 提案。
到 2019 年末,class 特性分佈在如下幾個提案和標準中:
JavaScript 類用構造函數初始化實例、定義字段和方法。你甚至可使用static
關鍵字在類上面附加字段和方法
繼承是經過 extends
關鍵字實現的:你能夠輕鬆地從父類建立子類。super
關鍵字用於子類訪問父類。
爲了利用封裝,讓字段和方法變成私有以便隱藏 class 的內部細節。私有字段和方法名必須以#
開頭。
JavaScript 中的類變得愈來愈方便使用了。
在私有屬性前加上#
前綴,你怎麼看?
做者:Dmitri Pavlutin
原文:dmitripavlutin.com/javascript-…
翻譯:1024譯站
更多前端技術乾貨盡在微信公衆號:1024譯站