前面的話
??大多數(shù)面向?qū)ο蟮木幊陶Z言都支持類和類繼承的特性毁枯,而
JS
卻不支持這些特性,只能通過其他方法定義并關(guān)聯(lián)多個(gè)相似的對(duì)象脑蠕,這種狀態(tài)一直延續(xù)到了ES5
薇搁。由于類似的庫層出不窮,最終還是在ECMAScript 6
中引入了類的特性爷速。本文將詳細(xì)介紹ES6
中的類
ES5近似結(jié)構(gòu)
在
ES5
中沒有類的概念,最相近的思路是創(chuàng)建一個(gè)自定義類型:首先創(chuàng)建一個(gè)構(gòu)造函數(shù)霞怀,然后定義另一個(gè)方法并賦值給構(gòu)造函數(shù)的原型
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
console.log(this.name);
};
let person = new PersonType("huochai");
person.sayName(); // 輸出 "huochai"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true
- 這段代碼中的
personType
是一個(gè)構(gòu)造函數(shù)惫东,其執(zhí)行后創(chuàng)建一個(gè)名為name
的屬性給personType
的原型添加一個(gè)sayName()
方法,所以PersonType
對(duì)象的所有實(shí)例都將共享這個(gè)方法毙石。然后使用new
操作符創(chuàng)建一個(gè)personType
的實(shí)例person
廉沮,并最終證實(shí)了person
對(duì)象確實(shí)是personType
的實(shí)例,且由于存在原型繼承的特性徐矩,因而它也是object
的實(shí)例滞时,許多模擬類的JS
庫都是基于這個(gè)模式進(jìn)行開發(fā),而且ES6
中的類也借鑒了類似的方法
類的聲明
ES6
有一種與其他語言中類似的類特性:類聲明滤灯。同時(shí)坪稽,它也是ES6
中最簡單的類形式
【基本的類聲明語法】
要聲明一個(gè)類曼玩,首先編寫
class
關(guān)鍵字,緊跟著的是類的名字窒百,其他部分的語法類似于對(duì)象字面量方法的簡寫形式黍判,但不需要在類的各元素之間使用逗號(hào)分隔
class PersonClass {
// 等價(jià)于 PersonType 構(gòu)造器
constructor(name) {
this.name = name;
}
// 等價(jià)于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("huochai");
person.sayName(); // 輸出 "huochai"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
通過類聲明語法定義
PersonClass
的行為與之前創(chuàng)建PersonType
構(gòu)造函數(shù)的過程相似,只是這里直接在類中通過特殊的constructor
方法名來定義構(gòu)造函數(shù)篙梢,且由于這種類使用簡潔語法來定義方法顷帖,因而不需要添加function
關(guān)鍵字。除constructor
外沒有其他保留的方法名渤滞,所以可以盡情添加方法私有屬性是實(shí)例中的屬性贬墩,不會(huì)出現(xiàn)在原型上,且只能在類的構(gòu)造函數(shù)或方法中創(chuàng)建妄呕,此例中的
name
就是一個(gè)私有屬性震糖。建議在構(gòu)造函數(shù)中創(chuàng)建所有私有屬性,從而只通過一處就可以控制類中的所有私有屬性類聲明僅僅是基于已有自定義類型聲明的語法糖趴腋。
typeof PersonClass
最終返回的結(jié)果是"function"
吊说,所以PersonClass
聲明實(shí)際上創(chuàng)建了一個(gè)具有構(gòu)造函數(shù)方法行為的函數(shù)。此示例中的sayName()
方法實(shí)際上是PersonClass.prototype
上的一個(gè)方法优炬;與之類似的是颁井,在之前的示例中,sayName()
也是personType.prototype
上的一個(gè)方法蠢护。通過語法糖包裝以后雅宾,類就可以代替自定義類型的功能,不必?fù)?dān)心使用的是哪種方法葵硕,只需關(guān)注如何定義正確的類
[注意]與函數(shù)不同的是眉抬,類屬性不可被賦予新值,在之前的示例中懈凹,PersonClass.prototype
就是這樣一個(gè)只可讀的類屬性
【為何使用類語法】
盡管類與自定義類型之間有諸多相似之處蜀变,但是它們之間仍然有一些差異
1、函數(shù)聲明可以被提升介评,而類聲明與let
聲明類似库北,不能被提升真正執(zhí)行聲明語句之前,它們會(huì)一直存在于臨時(shí)死區(qū)中
2们陆、類聲明中的所有代碼將自動(dòng)運(yùn)行在嚴(yán)格模式下寒瓦,而且無法強(qiáng)行讓代碼脫離嚴(yán)格模式執(zhí)行
3、在自定義類型中坪仇,需要通過Object.defineProperty()
方法手工指定某個(gè)方法為不可枚舉杂腰;而在類中,所有方法都是不可枚舉的
4椅文、每個(gè)類都有一個(gè)名為[[Construct]]
的內(nèi)部方法喂很,通過關(guān)鍵字new
調(diào)用那些不含[[Construct]]
的方法會(huì)導(dǎo)致程序拋出錯(cuò)誤
5惜颇、使用除關(guān)鍵字new
以外的方式調(diào)用類的構(gòu)造函數(shù)會(huì)導(dǎo)致程序拋出錯(cuò)誤
6、在類中修改類名會(huì)導(dǎo)致程序報(bào)錯(cuò)
- 了解了這些差異之后恤筛,可以用除了類之外的語法為之前示例中的
PersonClass
聲明編寫等價(jià)代碼
// 直接等價(jià)于 PersonClass
let PersonType2 = (function() {
"use strict";
const PersonType2 = function(name) {
// 確認(rèn)函數(shù)被調(diào)用時(shí)使用了 new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonType2.prototype, "sayName", {
value: function() {
// 確認(rèn)函數(shù)被調(diào)用時(shí)沒有使用 new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType2;
}());
這段代碼中有兩處
personType2
聲明:一處是外部作用域中的let
聲明官还,一處是立即執(zhí)行函數(shù)表達(dá)式(IIFE)
中的const
聲明,這也從側(cè)面說明了為什么可以在外部修改類名而內(nèi)部卻不可修改毒坛。在構(gòu)造函數(shù)中望伦,先檢查new.target
是否通過new
調(diào)用,如果不是則拋出錯(cuò)誤煎殷;緊接著屯伞,將sayName()
方法定義為不可枚舉,并再次檢查new.target
是否通過new
調(diào)用豪直,如果是則拋出錯(cuò)誤劣摇;最后,返回這個(gè)構(gòu)造函數(shù)盡管可以在不使用
new
語法的前提下實(shí)現(xiàn)類的所有功能弓乙,但如此一來末融,代碼變得極為復(fù)雜
【常量類名】
類的名稱只在類中為常量,所以盡管不能在類的方法中修改類名暇韧,但可以在外部修改
class Foo {
constructor() {
Foo = "bar"; // 執(zhí)行時(shí)拋出錯(cuò)誤
}
}// 但在類聲明之后沒問題Foo = "baz";
- 以上代碼中勾习,類的外部有一個(gè)
Foo
聲明,而類構(gòu)造函數(shù)里的Foo
則是一個(gè)獨(dú)立存在的綁定懈玻。內(nèi)部的Foo
就像是通過const
聲明的巧婶,修改它的值會(huì)導(dǎo)致程序拋出錯(cuò)誤;而外部的Foo
就像是通過let
聲明的涂乌,可以隨時(shí)修改這個(gè)綁定值
類表達(dá)式
類和函數(shù)都有兩種存在形式:聲明形式和表達(dá)式形式艺栈。聲明形式的函數(shù)和類都由相應(yīng)的關(guān)鍵字(分別為
function
和class
)進(jìn)行定義,隨后緊跟一個(gè)標(biāo)識(shí)符湾盒;表達(dá)式形式的函數(shù)和類與之類似湿右,只是不需要在關(guān)鍵字后添加標(biāo)識(shí)符
類表達(dá)式的設(shè)計(jì)初衷是為了聲明相應(yīng)變量或傳入函數(shù)作為參數(shù)
【基本的類表達(dá)式語法】
下面這段代碼等價(jià)于之前
PersonClass
示例的類表達(dá)式
let PersonClass = class {
// 等價(jià)于 PersonType 構(gòu)造器
constructor(name) {
this.name = name;
}
// 等價(jià)于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("huochai");
person.sayName(); // 輸出 "huochai"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
類聲明和類表達(dá)式僅在代碼編寫方式略有差異,二者均不會(huì)像函數(shù)聲明和函數(shù)表達(dá)式一樣被提升历涝,所以在運(yùn)行時(shí)狀態(tài)下無論選擇哪一種方式诅需,代碼最終的執(zhí)行結(jié)果都沒有太大差別
二者最重要的區(qū)別是
name
屬性不同,匿名類表達(dá)式的name
屬性值是一個(gè)空字符串荧库,而類聲明的name
屬性值為類名,例如赵刑,通過聲明方式定義一個(gè)類PersonClass
分衫,則PersonClass.name
的值為"PersonClass"
【命名類表達(dá)式】
類與函數(shù)一樣,都可以定義為命名表達(dá)式般此。聲明時(shí)蚪战,在關(guān)鍵字
class
后添加一個(gè)標(biāo)識(shí)符即可
let PersonClass = class PersonClass2 {
// 等價(jià)于 PersonType 構(gòu)造器
constructor(name) {
this.name = name;
}
// 等價(jià)于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"
- 上面的示例中牵现,類表達(dá)式被命名為
PersonClass2
,由于標(biāo)識(shí)符PersonClass2
只存在于類定義中邀桑,因此它可被用在像sayName()
這樣的方法中瞎疼。而在類的外部,由于不存在一個(gè)名為PersonClass2
的綁定壁畸,因而typeof PersonClass2
的值為"undefined"
// 直接等價(jià)于 PersonClass 具名的類表達(dá)式
let PersonClass = (function() {
"use strict";
const PersonClass2 = function(name) {
// 確認(rèn)函數(shù)被調(diào)用時(shí)使用了 new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonClass2.prototype, "sayName", {
value: function() {
// 確認(rèn)函數(shù)被調(diào)用時(shí)沒有使用 new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonClass2;
}());
在
JS
引擎中贼急,類表達(dá)式的實(shí)現(xiàn)與類聲明稍有不同。對(duì)于類聲明來說捏萍,通過let
定義的外部綁定與通過const
定義的內(nèi)部綁定具有相同名稱太抓;而命名類表達(dá)式通過const
定義名稱,從而PersonClass2
只能在類的內(nèi)部使用盡管命名類表達(dá)式與命名函數(shù)表達(dá)式有不同的表現(xiàn)令杈,但二者間仍有許多相似之處走敌,都可以在多個(gè)場景中作為值使用
一等公民
在程序中,一等公民是指一個(gè)可以傳入函數(shù)逗噩,可以從函數(shù)返回掉丽,并且可以賦值給變量的值。
JS
函數(shù)是一等公民(也被稱作頭等函數(shù))异雁,這也正是JS
中的一個(gè)獨(dú)特之處
ES6
延續(xù)了這個(gè)傳統(tǒng)捶障,將類也設(shè)計(jì)為一等公民,允許通過多種方式使用類的特性片迅。例如残邀,可以將類作為參數(shù)傳入函數(shù)中
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"
在這個(gè)示例中,調(diào)用
createObject()
函數(shù)時(shí)傳入一個(gè)匿名類表達(dá)式作為參數(shù)柑蛇,然后通過關(guān)鍵字new
實(shí)例化這個(gè)類并返回實(shí)例芥挣,將其儲(chǔ)存在變量obj
中類表達(dá)式還有另一種使用方式,通過立即調(diào)用類構(gòu)造函數(shù)可以創(chuàng)建單例耻台。用
new
調(diào)用類表達(dá)式空免,緊接著通過一對(duì)小括號(hào)調(diào)用這個(gè)表達(dá)式
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("huochai");
person.sayName(); // "huochai"
這里先創(chuàng)建一個(gè)匿名類表達(dá)式,然后立即執(zhí)行盆耽。依照這種模式可以使用類語法創(chuàng)建單例蹋砚,并且不會(huì)在作用域中暴露類的引用,其后的小括號(hào)表明正在調(diào)用一個(gè)函數(shù)摄杂,而且可以傳參數(shù)給這個(gè)函數(shù)
我們可以通過類似對(duì)象字面量的語法在類中創(chuàng)建訪問器屬性
訪問器屬性
盡管應(yīng)該在類構(gòu)造函數(shù)中創(chuàng)建自己的屬性坝咐,但是類也支持訪問器屬性。創(chuàng)建
getter
時(shí)析恢,需要在關(guān)鍵字get
后緊跟一個(gè)空格和相應(yīng)的標(biāo)識(shí)符墨坚;創(chuàng)建setter
時(shí),只需把關(guān)鍵字get
替換為set
即可
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false
- 這段代碼中的
CustomHTMLElement
類是一個(gè)針對(duì)現(xiàn)有DOM
元素的包裝器映挂,并通過getter
和setter
方法將這個(gè)元素的innerHTML
方法委托給html
屬性泽篮,這個(gè)訪問器屬性是在CustomHTMLElement.prototype
上創(chuàng)建的盗尸。與其他方法一樣,創(chuàng)建時(shí)聲明該屬性不可枚舉帽撑。下面這段代碼是非類形式的等價(jià)實(shí)現(xiàn)
// 直接等價(jià)于上個(gè)范例
let CustomHTMLElement = (function() {
"use strict";
const CustomHTMLElement = function(element) {
// 確認(rèn)函數(shù)被調(diào)用時(shí)使用了 new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.element = element;
}
Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});
return CustomHTMLElement;
}());
- 由上可見泼各,比起非類等效實(shí)現(xiàn),類語法可以節(jié)省很多代碼亏拉。在非類等效實(shí)現(xiàn)中扣蜻,僅
html
訪問器屬性定義的代碼量就與類聲明一樣多
可計(jì)算成員名稱
類和對(duì)象字面量還有更多相似之處,類方法和訪問器屬性也支持使用可計(jì)算名稱专筷。就像在對(duì)象字面量中一樣弱贼,用方括號(hào)包裹一個(gè)表達(dá)式即可使用可計(jì)算名稱
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass("huochai");
me.sayName(); // "huochai"
這個(gè)版本的
PersonClass
通過變量來給類定義中的方法命名,字符串"sayName"
被賦值給methodName
變量磷蛹,然后methodName
又被用于聲明隨后可直接訪問的sayName()
方法通過相同的方式可以在訪問器屬性中應(yīng)用可計(jì)算名稱
let propertyName = "html";
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
在這里通過
propertyName
變量并使用getter
和setter
方法為類添加html
屬性吮旅,并且可以像往常一樣通過.html
訪問該屬性在類和對(duì)象字面量諸多的共同點(diǎn)中,除了方法味咳、訪問器屬性及可計(jì)算名稱上的共同點(diǎn)外庇勃,還需要了解另一個(gè)相似之處,也就是生成器方法
生成器方法
在對(duì)象字面量中槽驶,可以通過在方法名前附加一個(gè)星號(hào)(*)的方式來定義生成器责嚷,在類中亦是如此,可以將任何方法定義成生成器
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
這段代碼創(chuàng)建了一個(gè)名為
MyClass
的類掂铐,它有一個(gè)生成器方法createIterator()
罕拂,其返回值為一個(gè)硬編碼在生成器中的迭代器。如果用對(duì)象來表示集合全陨,又希望通過簡單的方法迭代集合中的值爆班,那么生成器方法就派上用場了。數(shù)組辱姨、Set
集合及Map
集合為開發(fā)者們提供了多個(gè)生成器方法來與集合中的元素交互盡管生成器方法很實(shí)用柿菩,但如果類是用來表示值的集合的,那么為它定義一個(gè)默認(rèn)迭代器會(huì)更有用雨涛。通過
Symbol.iterator
定義生成器方法即可為類定義默認(rèn)迭代器
class Collection {
constructor() {
this.items = [];
}*[Symbol.iterator]() {
yield *this.items.values();
}
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
// 1
// 2
// 3
console.log(x);
}
這個(gè)示例用可計(jì)算名稱創(chuàng)建了一個(gè)代理
this.items
數(shù)組values()
迭代器的生成器方法枢舶。任何管理一系列值的類都應(yīng)該引入默認(rèn)迭代器,因?yàn)橐恍┡c特定集合有關(guān)的操作需要所操作的集合含有一個(gè)迭代器√婢茫現(xiàn)在可以將collection
的實(shí)例直接用于for-of
循環(huán)中或用展開運(yùn)算符操作它如果不介意在對(duì)象的實(shí)例中出現(xiàn)添加的方法和訪問器屬性凉泄,則可以將它們添加到類的原型中;如果希望它們只出現(xiàn)在類中蚯根,那么需要使用靜態(tài)成員
靜態(tài)成員
在
ES5
中旧困,直接將方法添加到構(gòu)造函數(shù)中來模擬靜態(tài)成員是一種常見的模式
function PersonType(name) {
this.name = name;
}
// 靜態(tài)方法
PersonType.create = function(name) {
return new PersonType(name);
};
// 實(shí)例方法
PersonType.prototype.sayName = function() {
console.log(this.name);
};
var person = PersonType.create("huochai");
- 在其他編程語言中,由于工廠方法
PersonType.create()
使用的數(shù)據(jù)不依賴personType
的實(shí)例稼锅,因而其會(huì)被認(rèn)為是一個(gè)靜態(tài)方法吼具。ES6
的類語法簡化了創(chuàng)建靜態(tài)成員的過程,在方法或訪問器屬性名前使用正式的靜態(tài)注釋即可
class PersonClass {
// 等價(jià)于 PersonType 構(gòu)造器
constructor(name) {
this.name = name;
}
// 等價(jià)于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等價(jià)于 PersonType.create
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("huochai");
-
PersonClass
定義只有一個(gè)靜態(tài)方法create()
矩距,它的語法與sayName()
的區(qū)別只在于是否使用static
關(guān)鍵字拗盒。類中的所有方法和訪問器屬性都可以用static
關(guān)鍵字來定義,唯一的限制是不能將static
用于定義構(gòu)造函數(shù)方法
[注意]不可在實(shí)例中訪問靜態(tài)成員锥债,必須要直接在類中訪問靜態(tài)成員
繼承與派生類
在
ES6
之前陡蝇,實(shí)現(xiàn)繼承與自定義類型是一個(gè)不小的工作。嚴(yán)格意義上的繼承需要多個(gè)步驟實(shí)現(xiàn)
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value:Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square
繼承自Rectangle
哮肚,為了這樣做登夫,必須用一個(gè)創(chuàng)建自Rectangle.prototype
的新對(duì)象重寫Square.prototype
并調(diào)用Rectangle.call()
方法。JS
新手經(jīng)常對(duì)這些步驟感到困惑允趟,即使是經(jīng)驗(yàn)豐富的開發(fā)者也常在這里出錯(cuò)類的出現(xiàn)讓我們可以更輕松地實(shí)現(xiàn)繼承功能恼策,使用熟悉的
extends
關(guān)鍵字可以指定類繼承的函數(shù)。原型會(huì)自動(dòng)調(diào)整潮剪,通過調(diào)用super()
方法即可訪問基類的構(gòu)造函數(shù)
class Rectangle {
constructor(length, width) {
this.length = length;this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// 與 Rectangle.call(this, length, length) 相同
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
這一次涣楷,
square
類通過extends
關(guān)鍵字繼承Rectangle
類,在square
構(gòu)造函數(shù)中通過super()
調(diào)用Rectangle
構(gòu)造函數(shù)并傳入相應(yīng)參數(shù)抗碰。請(qǐng)注意狮斗,與ES5
版本代碼不同的是,標(biāo)識(shí)符Rectangle
只用于類聲明(extends
之后)繼承自其他類的類被稱作派生類弧蝇,如果在派生類中指定了構(gòu)造函數(shù)則必須要調(diào)用
super()
碳褒,如果不這樣做程序就會(huì)報(bào)錯(cuò)。如果選擇不使用構(gòu)造函數(shù)看疗,則當(dāng)創(chuàng)建新的類實(shí)例時(shí)會(huì)自動(dòng)調(diào)用super()
并傳入所有參數(shù)
class Square extends Rectangle {
// 沒有構(gòu)造器
}
// 等價(jià)于:
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
- 示例中的第二個(gè)類是所有派生類的等效默認(rèn)構(gòu)造函數(shù)沙峻,所有參數(shù)按順序被傳遞給基類的構(gòu)造函數(shù)。這里展示的功能不太正確鹃觉,因?yàn)?code>square的構(gòu)造函數(shù)只需要一個(gè)參數(shù)专酗,所以最好手動(dòng)定義構(gòu)造函數(shù)
注意事項(xiàng)
使用
super()
時(shí)有以下幾個(gè)關(guān)鍵點(diǎn)
1、只可在派生類的構(gòu)造函數(shù)中使用super()
盗扇,如果嘗試在非派生類(不是用extends
聲明的類)或函數(shù)中使用則會(huì)導(dǎo)致程序拋出錯(cuò)誤
2祷肯、在構(gòu)造函數(shù)中訪問this
之前一定要調(diào)用super()
,它負(fù)責(zé)初始化this
疗隶,如果在調(diào)用super()
之前嘗試訪問this會(huì)導(dǎo)致程序出錯(cuò)
3佑笋、如果不想調(diào)用super()
,則唯一的方法是讓類的構(gòu)造函數(shù)返回一個(gè)對(duì)象
【類方法遮蔽】
派生類中的方法總會(huì)覆蓋基類中的同名方法斑鼻。比如給
square
添加getArea()
方法來重新定義這個(gè)方法的功能
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重寫并屏蔽 Rectangle.prototype.getArea()
getArea() {
return this.length * this.length;
}
}
- 由于為
square
定義了getArea()
方法蒋纬,便不能在square
的實(shí)例中調(diào)用Rectangle.prototype.getArea()
方法。當(dāng)然,如果想調(diào)用基類中的該方法蜀备,則可以調(diào)用super.getArea()
方法
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重寫关摇、屏蔽并調(diào)用了 Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
- 以這種方法使用
Super
,this
值會(huì)被自動(dòng)正確設(shè)置碾阁,然后就可以進(jìn)行簡單的方法調(diào)用了
【靜態(tài)成員繼承】
如果基類有靜態(tài)成員输虱,那么這些靜態(tài)成員在派生類中也可用。
JS
中的繼承與其他語言中的繼承一樣脂凶,只是在這里繼承還是一個(gè)新概念
class Rectangle {
constructor(length, width) {
this.length = length;this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
// 與 Rectangle.call(this, length, length) 相同
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false
- 在這段代碼中宪睹,新的靜態(tài)方法
create()
被添加到Rectangle
類中,繼承后的Square.create()
與Rectangle.create()
的行為很像
【派生自表達(dá)式的類】
ES6
最強(qiáng)大的一面或許是從表達(dá)式導(dǎo)出類的功能了蚕钦。只要表達(dá)式可以被解析為一個(gè)函數(shù)并且具有[[Construct]
屬性和原型亭病,那么就可以用extends
進(jìn)行派生
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
Rectangle
是一個(gè)ES5
風(fēng)格的構(gòu)造函數(shù),Square
是一個(gè)類嘶居,由于Rectangle
具有[[Construct]]
屬性和原型罪帖,因此Square
類可以直接繼承它extends
強(qiáng)大的功能使類可以繼承自任意類型的表達(dá)式,從而創(chuàng)造更多可能性食听,例如動(dòng)態(tài)地確定類的繼承目標(biāo)
function Rectangle(length, width) {
this.length = length;this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends getBase() {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
-
getBase()
函數(shù)是類聲明的一部分胸蛛,直接調(diào)用后返回Rectang?e
,此示例實(shí)現(xiàn)的功能與之前的示例等價(jià)樱报。由于可以動(dòng)態(tài)確定使用哪個(gè)基類葬项,因而可以創(chuàng)建不同的繼承方法
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
這個(gè)示例使用了
mixin
函數(shù)代替?zhèn)鹘y(tǒng)的繼承方法,它可以接受任意數(shù)量的mixin
對(duì)象作為參數(shù)迹蛤。首先創(chuàng)建一個(gè)函數(shù)base
民珍,再將每一個(gè)mixin
對(duì)象的屬性值賦值給base
的原型,最后minxin
函數(shù)返回這個(gè)base
函數(shù)盗飒,所以Square
類就可以基于這個(gè)返回的函數(shù)用extends
進(jìn)行擴(kuò)展嚷量。由于使用了extends
,因此在構(gòu)造函數(shù)中需要調(diào)用super()
Square
的實(shí)例擁有來自AreaMixin
對(duì)象的getArea()
方法和來自SerializableMixin
對(duì)象的serialize
方法,這都是通過原型繼承實(shí)現(xiàn)的逆趣,mixin()
函數(shù)會(huì)用所有mixin
對(duì)象的自有屬性動(dòng)態(tài)填充新函數(shù)的原型蝶溶。如果多個(gè)mixin
對(duì)象具有相同屬性,那么只有最后一個(gè)被添加的屬性被保留
[注意]在extends
后可以使用任意表達(dá)式宣渗,但不是所有表達(dá)式最終都能生成合法的類抖所。如果使用null
或生成器函數(shù)會(huì)導(dǎo)致錯(cuò)誤發(fā)生,類在這些情況下沒有[[Consturct]]
屬性痕囱,嘗試為其創(chuàng)建新的實(shí)例會(huì)導(dǎo)致程序無法調(diào)用[[Construct]]
而報(bào)錯(cuò)
【內(nèi)建對(duì)象的繼承】
自
JS
數(shù)組誕生以來田轧,一直都希望通過繼承的方式創(chuàng)建屬于自己的特殊數(shù)組。在ES5
中這幾乎是不可能的鞍恢,用傳統(tǒng)的繼承方式無法實(shí)現(xiàn)這樣的功能
// 內(nèi)置數(shù)組的行為
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// 在 ES5 中嘗試?yán)^承數(shù)組
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); // "red"
這段代碼最后
console.log()
的輸出結(jié)果與預(yù)期不符傻粘,MyArray
實(shí)例的length
和數(shù)值型屬性的行為與內(nèi)建數(shù)組中的不一致每窖,這是因?yàn)橥ㄟ^傳統(tǒng)JS
繼承形式實(shí)現(xiàn)的數(shù)組繼承沒有從Array.apply()
或原型賦值中繼承相關(guān)功能ES6
類語法的一個(gè)目標(biāo)是支持內(nèi)建對(duì)象繼承,因而ES6
中的類繼承模型與ES5
稍有不同弦悉,主要體現(xiàn)在兩個(gè)方面在
ES5
的傳統(tǒng)繼承方式中窒典,先由派生類型(如MyArray
)創(chuàng)建this
的值,然后調(diào)用基類型的構(gòu)造函數(shù)(如Array.apply()
方法)警绩。這也意味著崇败,this
的值開始指向MyArray
的實(shí)例,但是隨后會(huì)被來自Array
的其他屬性修飾ES6
中的類繼承則與之相反肩祥,先由基類(Array)
創(chuàng)建this
的值,然后派生類的構(gòu)造函數(shù)(MyArray)
再修改這個(gè)值缩膝。所以一開始可以通過this
訪問基類的所有內(nèi)建功能混狠,然后再正確地接收所有與之相關(guān)的功能
class MyArray extends Array {
// 空代碼塊
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
-
MyArray
直接繼承自Array
,其行為與Array
也很相似疾层,操作數(shù)值型屬性會(huì)更新length
屬性将饺,操作length
屬性也會(huì)更新數(shù)值型屬性。于是痛黎,可以正確地繼承Array
對(duì)象來創(chuàng)建自己的派生數(shù)組類型予弧,當(dāng)然也可以繼承其他的內(nèi)建對(duì)象
【Symbol.species屬性】
內(nèi)建對(duì)象繼承的一個(gè)實(shí)用之處是,原本在內(nèi)建對(duì)象中返回實(shí)例自身的方法將自動(dòng)返回派生類的實(shí)例湖饱。所以掖蛤,如果有一個(gè)繼承自
Array
的派生類MyArray
,那么像slice()
這樣的方法也會(huì)返回一個(gè)MyArray
的實(shí)例
class MyArray extends Array {
// 空代碼塊
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
正常情況下井厌,繼承自
Array
的slice()
方法應(yīng)該返回Array
的實(shí)例蚓庭,但是在這段代碼中,slice()
方法返回的是MyArray
的實(shí)例仅仆。在瀏覽器引擎背后是通Symbol.species
屬性實(shí)現(xiàn)這一行為Symbol.species
是諸多內(nèi)部Symbol
中的一個(gè)器赞,它被用于定義返回函數(shù)的靜態(tài)訪問器屬性。被返回的函數(shù)是一個(gè)構(gòu)造函數(shù)墓拜,每當(dāng)要在實(shí)例的方法中(不是在構(gòu)造函數(shù)中)創(chuàng)建類的實(shí)例時(shí)必須使用這個(gè)構(gòu)造函數(shù)港柜。以下這些內(nèi)建類型均己定義Symbol.species
屬性
Array
ArrayBuffer
Map
Promise
RegExp
Set
Typed arrays
- 列表中的每個(gè)類型都有一個(gè)默認(rèn)的
symbol.species
屬性,該屬性的返回值為this
咳榜,這也意味著該屬性總會(huì)返回構(gòu)造函數(shù)
// 幾個(gè)內(nèi)置類型使用 species 的方式類似于此
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
- 在這個(gè)示例中夏醉,
Symbol.species
被用來給MyClass
賦值靜態(tài)訪問器屬性。這里只有一個(gè)getter
方法卻沒有setter
方法贿衍,這是因?yàn)樵谶@里不可以改變類的種類授舟。調(diào)用this.constructor[Symbol.species]
會(huì)返回MyClass
,clone()
方法通過這個(gè)定義可以返回新的實(shí)例贸辈,從而允許派生類覆蓋這個(gè)值
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// 空代碼塊
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
在這里释树,
MyDerivedClass1
繼承MyClass
時(shí)未改變Symbol.species
屬性肠槽,由于this.constructor[Symbol.species]
的返回值是MyDerivedClass1
,因此調(diào)用clone()
返回的是MyDerivedClass1
的實(shí)例奢啥;MyDerivedClass2
繼承MyClass
時(shí)重寫了Symbol.species
讓其返回MyClass
秸仙,調(diào)用MyDerivedClass2
實(shí)例的clone()
方法時(shí),返回值是一個(gè)MyClass
的實(shí)例桩盲。通過Symbol.species
可以定義當(dāng)派生類的方法返回實(shí)例時(shí)寂纪,應(yīng)該返回的值的類型數(shù)組通過
Symbol.species
來指定那些返回?cái)?shù)組的方法應(yīng)當(dāng)從哪個(gè)類中獲取。在一個(gè)派生自數(shù)組的類中赌结,可以決定繼承的方法返回何種類型的對(duì)象
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false
這段代碼重寫了
MyArray
繼承自Array
的Symbol.species
屬性捞蛋,所有返回?cái)?shù)組的繼承方法現(xiàn)在將使用Array
的實(shí)例,而不使用MyArray
的實(shí)例一般來說柬姚,只要想在類方法中調(diào)用
this.constructor
拟杉,就應(yīng)該使用Symbol.species
屬性,從而讓派生類重寫返回類型量承。而且如果正從一個(gè)已定義Symbol.species
屬性的類創(chuàng)建派生類搬设,那么要確保使用那個(gè)值而不是使用構(gòu)造函數(shù)
【在類的構(gòu)造函數(shù)中使用new.target】
new.target
及它的值根據(jù)函數(shù)被調(diào)用的方式而改變。在類的構(gòu)造函數(shù)中也可以通過new.target
來確定類是如何被調(diào)用的撕捍。簡單情況下拿穴,new.target
等于類的構(gòu)造函數(shù)
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;this.width = width;
}
}
// new.target 就是 Rectanglevar
obj = new Rectangle(3, 4); // 輸出 true
- 這段代碼展示了當(dāng)調(diào)用
new Rectangle(3.4)
時(shí)等價(jià)于Rectangle
的new.target
。類構(gòu)造函數(shù)必須通過new
關(guān)鍵字調(diào)用忧风,所以總是在類的構(gòu)造函數(shù)中定義new.target
屬性默色,但是其值有時(shí)會(huì)不同
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
// new.target 就是 Squarevar
obj = new Square(3); // 輸出 false
-
Square
調(diào)用Rectangle
的構(gòu)造函數(shù),所以當(dāng)調(diào)用發(fā)生時(shí)new.target
等于Square
阀蒂。這一點(diǎn)非常重要该窗,因?yàn)槊總€(gè)構(gòu)造函數(shù)都可以根據(jù)自身被調(diào)用的方式改變自己的行為
// 靜態(tài)的基類
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("This class cannot be instantiated directly.")
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;this.width = width;
}
}
var x = new Shape(); // 拋出錯(cuò)誤
var y = new Rectangle(3, 4); // 沒有錯(cuò)誤
console.log(y instanceof Shape); // true
- 在這個(gè)示例中,每當(dāng)
new.target
是Shape
時(shí)構(gòu)造函數(shù)總會(huì)拋出錯(cuò)誤蚤霞,這相當(dāng)于調(diào)用new Shape()
時(shí)總會(huì)出錯(cuò)酗失。但是,仍可用Shape
作為基類派生其他類昧绣,示例中的Rectangle
便是這樣规肴。super()
調(diào)用執(zhí)行了Shape
的構(gòu)造函數(shù),new.target
與Rectangle
等價(jià)夜畴,所以構(gòu)造函數(shù)繼續(xù)執(zhí)行不會(huì)拋出錯(cuò)誤
[注意]因?yàn)轭惐仨毻ㄟ^new
關(guān)鍵字才能調(diào)用嘹悼,所以在類的構(gòu)造函數(shù)中庐扫,new.target
屬性永遠(yuǎn)不會(huì)是undefined
其他章節(jié)
- ES6-數(shù)字?jǐn)U展
- ES6-字符串拓展
- ES6-模板字面量
- ES6-關(guān)于Unicode的相關(guān)擴(kuò)展
- ES6-正則表達(dá)式擴(kuò)展
- ES6-函數(shù)擴(kuò)展
- ES6-對(duì)象擴(kuò)展
- ES6-Symbol
- ES6-Set和Map集合
- ES6-數(shù)組擴(kuò)展
- ES6-定型數(shù)組
- ES6-塊級(jí)作用域
- ES6-解構(gòu)賦值
- ES6-類
- ES6-代理(Proxy)和反射(Reflection)
- ES6-ES6中的模塊
- ES6-ES2017中的修飾器Decorator
- ES6-迭代器(Iterator)和生成器(Generator)
- ES6-Promise和異步編程
- ES6-ES2017中的async