本文繼續(xù)對JavaScript高級程序設(shè)計第四版 第八章 對象伴郁、類與面向?qū)ο缶幊?進行學(xué)習
一、繼承
繼承是面向?qū)ο缶幊讨杏懻撟疃嗟脑掝}金吗。很多面向?qū)ο笳Z言都支持兩種繼承:接口繼承和實現(xiàn)繼承。前者只繼承方法簽名勋颖,后者繼承實際的方法。接口繼承在 ECMAScript 中是不可能的勋锤,因為函數(shù)沒有簽名牙言。實現(xiàn)繼承是 ECMAScript 唯一支持的繼承方式,而這主要是通過原型鏈實現(xiàn)的怪得。
關(guān)于接口繼承與實現(xiàn)繼承,可以參考C++ 接口繼承與實現(xiàn)繼承的區(qū)別和選擇
1.原型鏈
ECMA-262 把原型鏈定義為 ECMAScript 的主要繼承方式卑硫。其基本思想就是通過原型繼承多個引用類型的屬性和方法徒恋。重溫一下構(gòu)造函數(shù)、原型和實例的關(guān)系:每個構(gòu)造函數(shù)都有一個原型對象欢伏,原型有一個屬性指回構(gòu)造函數(shù)入挣,而實例有一個內(nèi)部指針指向原型。如果原型是另一個類型的實例呢硝拧?那就意味著這個原型本身有一個內(nèi)部指針指向另一個原型径筏,相應(yīng)地另一個原型也有一個指針指向另一個構(gòu)造函數(shù)。這樣就在實例和原型之間構(gòu)造了一條原型鏈障陶。這就是原型鏈的基本構(gòu)想滋恬。
實現(xiàn)原型鏈涉及如下代碼模式:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 繼承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
以上代碼定義了兩個類型:SuperType 和 SubType。這兩個類型分別定義了一個屬性和一個方法抱究。這兩個類型的主要區(qū)別是 SubType 通過創(chuàng)建 SuperType 的實例并將其賦值給自己的原型 SubTtype.prototype 實現(xiàn)了對 SuperType 的繼承恢氯。這個賦值重寫了 SubType 最初的原型,將其替換為SuperType 的實例鼓寺。這意味著 SuperType 實例可以訪問的所有屬性和方法也會存在于 SubType.prototype勋拟。這樣實現(xiàn)繼承之后,代碼緊接著又給 SubType.prototype妈候,也就是這個 SuperType 的實例添加了一個新方法敢靡。最后又創(chuàng)建了 SubType 的實例并調(diào)用了它繼承的 getSuperValue()方法。
原型鏈擴展了前面描述的原型搜索機制苦银。我們知道啸胧,在讀取實例上的屬性時,首先會在實例上搜索這個屬性幔虏。如果沒找到吓揪,則會繼承搜索實例的原型。在通過原型鏈實現(xiàn)繼承之后所计,搜索就可以繼承向上柠辞,搜索原型的原型。對前面的例子而言主胧,調(diào)用 instance.getSuperValue()經(jīng)過了 3 步搜索:instance、SubType.prototype 和 SuperType.prototype,最后一步才找到這個方法著拭。對屬性和方法的搜索會一直持續(xù)到原型鏈的末端载佳。
2. 默認原型
實際上,原型鏈中還有一環(huán)硫狞。默認情況下,所有引用類型都繼承自 Object,這也是通過原型鏈實現(xiàn)的予颤。任何函數(shù)的默認原型都是一個 Object 的實例,這意味著這個實例有一個內(nèi)部指針指向Object.prototype冬阳。這也是為什么自定義類型能夠繼承包括 toString()蛤虐、valueOf()在內(nèi)的所有默認方法的原因。
3.原型與繼承關(guān)系
原型與實例的關(guān)系可以通過兩種方式來確定肝陪。第一種方式是使用 instanceof 操作符驳庭,如果一個實例的原型鏈中出現(xiàn)過相應(yīng)的構(gòu)造函數(shù),則 instanceof 返回 true氯窍。如下例所示:
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
從技術(shù)上講饲常,instance 是 Object、SuperType 和 SubType 的實例狼讨,因為 instance 的原型鏈中包含這些構(gòu)造函數(shù)的原型贝淤。結(jié)果就是 instanceof 對所有這些構(gòu)造函數(shù)都返回 true。
確定這種關(guān)系的第二種方式是使用 isPrototypeOf()方法政供。原型鏈中的每個原型都可以調(diào)用這個
方法霹娄,如下例所示,只要原型鏈中包含這個原型鲫骗,這個方法就返回 true:
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
4.關(guān)于方法
子類有時候需要覆蓋父類的方法犬耻,或者增加父類沒有的方法。為此执泰,這些方法必須在原型賦值之后再添加到原型上枕磁。來看下面的例子:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 繼承 SuperType
SubType.prototype = new SuperType();
// 新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
// 覆蓋已有的方法
SubType.prototype.getSuperValue = function () {
return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false
5.原型鏈的問題
原型鏈雖然是實現(xiàn)繼承的強大工具,但它也有問題术吝。主要問題出現(xiàn)在原型中包含引用值的時候计济。前面在談到原型的問題時也提到過,原型中包含的引用值會在所有實例間共享排苍,這也是為什么屬性通常會在構(gòu)造函數(shù)中定義而不會定義在原型上的原因沦寂。在使用原型實現(xiàn)繼承時,原型實際上變成了另一個類型的實例淘衙。這意味著原先的實例屬性搖身一變成為了原型屬性传藏。下面的例子揭示了這個問題:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 繼承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"
原型鏈的第二個問題是,子類型在實例化時不能給父類型的構(gòu)造函數(shù)傳參。事實上毯侦,我們無法在不影響所有對象實例的情況下把參數(shù)傳進父類的構(gòu)造函數(shù)哭靖。再加上之前提到的原型中包含引用值的問題,就導(dǎo)致原型鏈基本不會被單獨使用侈离。
二试幽、盜用構(gòu)造函數(shù)
為了解決原型包含引用值導(dǎo)致的繼承問題,一種叫作“盜用構(gòu)造函數(shù)”(constructor stealing)的技術(shù)在開發(fā)社區(qū)流行起來(這種技術(shù)有時也稱作“對象偽裝”或“經(jīng)典繼承”)卦碾∑涛耄基本思路很簡單:在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)。因為畢竟函數(shù)就是在特定上下文中執(zhí)行代碼的簡單對象洲胖,所以可以使用apply()和 call()方法以新創(chuàng)建的對象為上下文執(zhí)行構(gòu)造函數(shù)济榨。
1.簡單介紹一下apply,call:
apply call可以在特定的作用域內(nèi)調(diào)用函數(shù),唯一區(qū)別是接收參數(shù)的不同宾濒。第一個參數(shù)都是運行函數(shù)的作用域,apply的第二個參數(shù)是一個數(shù)組,call則是把其余所有參數(shù)直接傳遞屏箍。
function sum(sum1,sum2){
}
function callSum2(sum1,sum2){
return sum.apply(this,[sum1,sum2]);
}
function callSum(sum1,sum2){
return sum.call(this,num1,num2);
}
2.盜用構(gòu)造函數(shù)例子
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 繼承 SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
示例中加粗的代碼展示了盜用構(gòu)造函數(shù)的調(diào)用绘梦。通過使用 call()(或 apply())方法,SuperType構(gòu)造函數(shù)在為 SubType 的實例創(chuàng)建的新對象的上下文中執(zhí)行了赴魁。這相當于新的 SubType 對象上運行了SuperType()函數(shù)中的所有初始化代碼卸奉。結(jié)果就是每個實例都會有自己的 colors 屬性。
3.傳遞參數(shù)
相比于使用原型鏈颖御,盜用構(gòu)造函數(shù)的一個優(yōu)點就是可以在子類構(gòu)造函數(shù)中向父類構(gòu)造函數(shù)傳參榄棵。來看下面的例子:
function SuperType(name){
this.name = name;
}
function SubType() {
// 繼承 SuperType 并傳參
SuperType.call(this, "Nicholas");
// 實例屬性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
4.盜用構(gòu)造函數(shù)的問題
盜用構(gòu)造函數(shù)的主要缺點,也是使用構(gòu)造函數(shù)模式自定義類型的問題:必須在構(gòu)造函數(shù)中定義方法潘拱,因此函數(shù)不能重用疹鳄。此外,子類也不能訪問父類原型上定義的方法芦岂,因此所有類型只能使用構(gòu)造函數(shù)模式瘪弓。由于存在這些問題,盜用構(gòu)造函數(shù)基本上也不能單獨使用禽最。
三腺怯、組合繼承
組合繼承(有時候也叫偽經(jīng)典繼承)綜合了原型鏈和盜用構(gòu)造函數(shù),將兩者的優(yōu)點集中了起來川无∏赫迹基本的思路是使用原型鏈繼承原型上的屬性和方法,而通過盜用構(gòu)造函數(shù)繼承實例屬性懦趋。這樣既可以把方法定義在原型上以實現(xiàn)重用晾虑,又可以讓每個實例都有自己的屬性。來看下面的例子:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 繼承屬性
SuperType.call(this, name);
this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
組合繼承彌補了原型鏈和盜用構(gòu)造函數(shù)的不足,是 JavaScript 中使用最多的繼承模式走贪。而且組合繼承也保留了 instanceof 操作符和 isPrototypeOf()方法識別合成對象的能力佛猛。
四、原型式繼承Object.create()
2006 年坠狡,Douglas Crockford 寫了一篇文章:《JavaScript 中的原型式繼承》(“Prototypal Inheritance in JavaScript”)继找。這篇文章介紹了一種不涉及嚴格意義上構(gòu)造函數(shù)的繼承方法。他的出發(fā)點是即使不自定義類型也可以通過原型實現(xiàn)對象之間的信息共享逃沿。文章最終給出了一個函數(shù):
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
這個 object()函數(shù)會創(chuàng)建一個臨時構(gòu)造函數(shù)婴渡,將傳入的對象賦值給這個構(gòu)造函數(shù)的原型,然后返回這個臨時類型的一個實例凯亮。本質(zhì)上边臼,object()是對傳入的對象執(zhí)行了一次淺復(fù)制。來看下面的例子:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Crockford 推薦的原型式繼承適用于這種情況:你有一個對象假消,想在它的基礎(chǔ)上再創(chuàng)建一個新對象柠并。你需要把這個對象先傳給 object(),然后再對返回的對象進行適當修改富拗。在這個例子中臼予,person 對象定義了另一個對象也應(yīng)該共享的信息,把它傳給 object()之后會返回一個新對象啃沪。這個新對象的原型是 person粘拾,意味著它的原型上既有原始值屬性又有引用值屬性。這也意味著 person.friends 不僅是person 的屬性创千,也會跟 anotherPerson 和 yetAnotherPerson 共享缰雇。這里實際上克隆了兩個 person。
ECMAScript 5 通過增加 Object.create()方法將原型式繼承的概念規(guī)范化了追驴。這個方法接收兩個參數(shù):作為新對象原型的對象械哟,以及給新對象定義額外屬性的對象(第二個可選)。在只有一個參數(shù)時殿雪,Object.create()與這里的 object()方法效果相同:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create()的第二個參數(shù)與 Object.defineProperties()的第二個參數(shù)一樣:每個新增屬性都通過各自的描述符來描述戒良。以這種方式添加的屬性會遮蔽原型對象上的同名屬性。比如:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // "Greg"
原型式繼承非常適合不需要單獨創(chuàng)建構(gòu)造函數(shù)冠摄,但仍然需要在對象間共享信息的場合糯崎。但要記住,屬性中包含的引用值始終會在相關(guān)對象間共享河泳,跟使用原型模式是一樣的沃呢。
五、寄生式繼承
與原型式繼承比較接近的一種繼承方式是寄生式繼承(parasitic inheritance)拆挥,也是 Crockford 首倡的一種模式薄霜。寄生式繼承背后的思路類似于寄生構(gòu)造函數(shù)和工廠模式:創(chuàng)建一個實現(xiàn)繼承的函數(shù)某抓,以某種方式增強對象,然后返回這個對象惰瓜》窀保基本的寄生繼承模式如下:
function createAnother(original){
let clone = object(original); // 通過調(diào)用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function() { // 以某種方式增強這個對象
console.log("hi");
};
return clone; // 返回這個對象
}
在這段代碼中,createAnother()函數(shù)接收一個參數(shù)崎坊,就是新對象的基準對象备禀。這個對象 original會被傳給 object()函數(shù),然后將返回的新對象賦值給 clone奈揍。接著給 clone 對象添加一個新方法sayHi()曲尸。最后返回這個對象∧泻玻可以像下面這樣使用 createAnother()函數(shù):
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
這個例子基于 person 對象返回了一個新對象另患。新返回的 anotherPerson 對象具有 person 的所有屬性和方法,還有一個新方法叫 sayHi()蛾绎。
寄生式繼承同樣適合主要關(guān)注對象昆箕,而不在乎類型和構(gòu)造函數(shù)的場景。object()函數(shù)不是寄生式繼承所必需的租冠,任何返回新對象的函數(shù)都可以在這里使用鹏倘。注意 通過寄生式繼承給對象添加函數(shù)會導(dǎo)致函數(shù)難以重用,與構(gòu)造函數(shù)模式類似肺稀。
六第股、寄生式組合繼承
組合繼承其實也存在效率問題应民。最主要的效率問題就是父類構(gòu)造函數(shù)始終會被調(diào)用兩次:一次在是創(chuàng)建子類原型時調(diào)用话原,另一次是在子類構(gòu)造函數(shù)中調(diào)用。本質(zhì)上诲锹,子類原型最終是要包含超類對象的所有實例屬性繁仁,子類構(gòu)造函數(shù)只要在執(zhí)行時重寫自己的原型就行了。再來看一看這個組合繼承的例子:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
SuperType.call(this, name); // 第二次調(diào)用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次調(diào)用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
代碼中加粗的部分是調(diào)用 SuperType 構(gòu)造函數(shù)的地方归园。在上面的代碼執(zhí)行后黄虱,SubType.prototype上會有兩個屬性:name 和 colors。它們都是 SuperType 的實例屬性庸诱,但現(xiàn)在成為了 SubType 的原型屬性捻浦。在調(diào)用 SubType 構(gòu)造函數(shù)時,也會調(diào)用 SuperType 構(gòu)造函數(shù)桥爽,這一次會在新對象上創(chuàng)建實例屬性 name 和 colors朱灿。
寄生式組合繼承通過盜用構(gòu)造函數(shù)繼承屬性,但使用混合式原型鏈繼承方法钠四〉涟牵基本思路是不通過調(diào)用父類構(gòu)造函數(shù)給子類原型賦值,而是取得父類原型的一個副本。說到底就是使用寄生式繼承來繼承父類原型侣灶,然后將返回的新對象賦值給子類原型甸祭。寄生式組合繼承的基本模式如下所示:
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 創(chuàng)建對象
prototype.constructor = subType; // 增強對象
subType.prototype = prototype; // 賦值對象
}
這個 inheritPrototype()函數(shù)實現(xiàn)了寄生式組合繼承的核心邏輯。這個函數(shù)接收兩個參數(shù):子類構(gòu)造函數(shù)和父類構(gòu)造函數(shù)褥影。在這個函數(shù)內(nèi)部池户,第一步是創(chuàng)建父類原型的一個副本。然后伪阶,給返回的prototype 對象設(shè)置 constructor 屬性煞檩,解決由于重寫原型導(dǎo)致默認 constructor 丟失的問題。最后將新創(chuàng)建的對象賦值給子類型的原型栅贴。如下例所示斟湃,調(diào)用 inheritPrototype()就可以實現(xiàn)前面例子中的子類型原型賦值:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
這里只調(diào)用了一次 SuperType 構(gòu)造函數(shù),避免了 SubType.prototype 上不必要也用不到的屬性檐薯,因此可以說這個例子的效率更高凝赛。而且,原型鏈仍然保持不變坛缕,因此 instanceof 操作符和isPrototypeOf()方法正常有效墓猎。寄生式組合繼承可以算是引用類型繼承的最佳模式。
七赚楚、類
前幾節(jié)深入講解了如何只使用 ECMAScript 5 的特性來模擬類似于類(class-like)的行為毙沾。不難看出,各種策略都有自己的問題宠页,也有相應(yīng)的妥協(xié)左胞。正因為如此,實現(xiàn)繼承的代碼也顯得非常冗長和混亂举户。
為解決這些問題烤宙,ECMAScript 6 新引入的 class 關(guān)鍵字具有正式定義類的能力。類(class)是ECMAScript 中新的基礎(chǔ)性語法糖結(jié)構(gòu)俭嘁,因此剛開始接觸時可能會不太習慣躺枕。雖然 ECMAScript 6 類表面上看起來可以支持正式的面向?qū)ο缶幊蹋珜嶋H上它背后使用的仍然是原型和構(gòu)造函數(shù)的概念供填。
1. 實例化
使用 new 操作符實例化 Person 的操作等于使用 new 調(diào)用其構(gòu)造函數(shù)拐云。唯一可感知的不同之處就是,JavaScript 解釋器知道使用 new 和類意味著應(yīng)該使用 constructor 函數(shù)進行實例化近她。使用 new 調(diào)用類的構(gòu)造函數(shù)會執(zhí)行如下操作叉瘩。
- (1) 在內(nèi)存中創(chuàng)建一個新對象。
- (2) 這個新對象內(nèi)部的[[Prototype]]指針被賦值為構(gòu)造函數(shù)的 prototype 屬性泄私。
- (3) 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)房揭。
- (4) 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性)备闲。
- (5) 如果構(gòu)造函數(shù)返回非空對象,則返回該對象捅暴;否則恬砂,返回剛創(chuàng)建的新對象。
來看下面的例子:
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
this.color = 'orange';
}
}
let a = new Animal();
let p = new Person(); // person ctor
let v = new Vegetable();
console.log(v.color); // orange
2. 把類當成特殊函數(shù)
ECMAScript 中沒有正式的類這個類型蓬痒。從各方面來看泻骤,ECMAScript 類就是一種特殊函數(shù)。聲明一個類之后梧奢,通過 typeof 操作符檢測類標識符狱掂,表明它是一個函數(shù):
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
類標識符有 prototype 屬性,而這個原型也有一個 constructor 屬性指向類自身:
class Person{}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true
與普通構(gòu)造函數(shù)一樣亲轨,可以使用 instanceof 操作符檢查構(gòu)造函數(shù)原型是否存在于實例的原型鏈中:
class Person {}
let p = new Person();
console.log(p instanceof Person); // true
八趋惨、實例、原型和類成員
類的語法可以非常方便地定義應(yīng)該存在于實例上的成員惦蚊、應(yīng)該存在于原型上的成員器虾,以及應(yīng)該存在于類本身的成員。
1.實例成員
每個實例都對應(yīng)一個唯一的成員對象蹦锋,這意味著所有成員都不會在原型上共享:
class Person {
constructor() {
// 這個例子先使用對象包裝類型定義一個字符串
// 為的是在下面測試兩個對象的相等性
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
2.原型方法與訪問器
為了在實例間共享方法兆沙,類定義語法把在類塊中定義的方法作為原型方法。
class Person {
constructor() {
// 添加到 this 的所有內(nèi)容都會存在于不同的實例上
this.locate = () => console.log('instance');
}
// 在類塊中定義的所有內(nèi)容都會定義在類的原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
類定義也支持獲取和設(shè)置訪問器莉掂。語法與行為跟普通對象一樣:
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake
3.靜態(tài)類方法
可以在類上定義靜態(tài)方法葛圃。這些方法通常用于執(zhí)行不特定于實例的操作,也不要求存在類的實例憎妙。與原型成員類似库正,靜態(tài)成員每個類上只能有一個。靜態(tài)類成員在類定義中使用 static 關(guān)鍵字作為前綴尚氛。在靜態(tài)成員中诀诊,this 引用類自身洞渤。其他所有約定跟原型成員一樣:
class Person {
constructor() {
// 添加到 this 的所有內(nèi)容都會存在于不同的實例上
this.locate = () => console.log('instance', this);
}
// 定義在類的原型對象上
locate() {
console.log('prototype', this);
}
// 定義在類本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
靜態(tài)類方法非常適合作為實例工廠:
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 使用隨機年齡創(chuàng)建并返回一個 Person 實例
return new Person(Math.floor(Math.random()*100));
}
}
console.log(Person.create()); // Person { age_: ... }
4.非函數(shù)原型和類成員
雖然類定義并不顯式支持在原型或類上添加成員數(shù)據(jù)阅嘶,但在類定義外部,可以手動添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在類上定義數(shù)據(jù)成員
Person.greeting = 'My name is';
// 在原型上定義數(shù)據(jù)成員
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake
注意 類定義中之所以沒有顯式支持添加數(shù)據(jù)成員载迄,是因為在共享目標(原型和類)上添加可變(可修改)數(shù)據(jù)成員是一種反模式讯柔。一般來說,對象實例應(yīng)該獨自擁有通過 this 引用的數(shù)據(jù)护昧。
5.迭代器與生成器方法
類定義語法支持在原型和類本身上定義生成器方法:
class Person {
// 在原型上定義生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在類上定義生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
因為支持生成器方法魂迄,所以可以通過添加一個默認的迭代器,把類實例變成可迭代對象:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
也可以只返回迭代器實例:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
九惋耙、繼承
本章前面花了大量篇幅討論如何使用 ES5 的機制實現(xiàn)繼承捣炬。ECMAScript 6 新增特性中最出色的一個就是原生支持了類繼承機制熊昌。雖然類繼承使用的是新語法,但背后依舊使用的是原型鏈湿酸。
1.繼承基礎(chǔ)
ES6 類支持單繼承婿屹。使用 extends 關(guān)鍵字,就可以繼承任何擁有[[Construct]]和原型的對象推溃。很大程度上昂利,這意味著不僅可以繼承一個類,也可以繼承普通的構(gòu)造函數(shù)(保持向后兼容):
class Vehicle {}
// 繼承類
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 繼承普通構(gòu)造函數(shù)
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
2.構(gòu)造函數(shù)铁坎、HomeObject 和 super()
派生類的方法可以通過 super 關(guān)鍵字引用它們的原型蜂奸。這個關(guān)鍵字只能在派生類中使用,而且僅限于類構(gòu)造函數(shù)硬萍、實例方法和靜態(tài)方法內(nèi)部扩所。在類構(gòu)造函數(shù)中使用 super 可以調(diào)用父類構(gòu)造函數(shù)。
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不要在調(diào)用 super()之前引用 this朴乖,否則會拋出 ReferenceError
super(); // 相當于 super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
new Bus();
在靜態(tài)方法中可以通過 super 調(diào)用繼承的類上定義的靜態(tài)方法:
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
3.抽象基類
有時候可能需要定義這樣一個類碌奉,它可供其他類繼承,但本身不會被實例化寒砖。雖然 ECMAScript 沒有專門支持這種類的語法 赐劣,但通過 new.target 也很容易實現(xiàn)。new.target 保存通過 new 關(guān)鍵字調(diào)用的類或函數(shù)哩都。通過在實例化時檢測 new.target 是不是抽象基類魁兼,可以阻止對抽象基類的實例化:
// 抽象基類
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
}
}
// 派生類
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
另外,通過在抽象基類構(gòu)造函數(shù)中進行檢查漠嵌,可以要求派生類必須定義某個方法咐汞。因為原型方法在調(diào)用類構(gòu)造函數(shù)之前就已經(jīng)存在了,所以可以通過 this 關(guān)鍵字來檢查相應(yīng)的方法:
// 抽象基類
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生類
class Bus extends Vehicle {
foo() {}
}
// 派生類
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
4.繼承內(nèi)置類型
ES6 類為繼承內(nèi)置引用類型提供了順暢的機制儒鹿,開發(fā)者可以方便地擴展內(nèi)置類型:
class SuperArray extends Array {
shuffle() {
// 洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]
5.類混入
把不同類的行為集中到一個類是一種常見的 JavaScript 模式化撕。雖然 ES6 沒有顯式支持多類繼承,但通過現(xiàn)有特性可以輕松地模擬這種行為约炎。一個策略是定義一組“可嵌套”的函數(shù)植阴,每個函數(shù)分別接收一個超類作為參數(shù),而將混入類定義為這個參數(shù)的子類圾浅,并返回這個類掠手。這些組合函數(shù)可以連綴調(diào)用,最終組合成超類表達式:
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
注意 Object.assign()方法是為了混入對象行為而設(shè)計的狸捕。只有在需要混入類的行為時才有必要自己實現(xiàn)混入表達式喷鸽。如果只是需要混入多個對象的屬性,那么使用Object.assign()就可以了灸拍。
注意 很多 JavaScript 框架(特別是 React)已經(jīng)拋棄混入模式做祝,轉(zhuǎn)向了組合模式(把方法提取到獨立的類和輔助對象中砾省,然后把它們組合起來,但不使用繼承)混槐。這反映了那個眾所周知的軟件設(shè)計原則:“組合勝過繼承(composition over inheritance)纯蛾。”這個設(shè)計原則被很多人遵循纵隔,在代碼設(shè)計中能提供極大的靈活性翻诉。