js紅寶書筆記九 第八章 類與面向?qū)ο缶幊?/h1>

本文繼續(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è)計中能提供極大的靈活性翻诉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者

  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捌刮,隨后出現(xiàn)的幾起案子碰煌,更是在濱河造成了極大的恐慌,老刑警劉巖绅作,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芦圾,死亡現(xiàn)場離奇詭異,居然都是意外死亡俄认,警方通過查閱死者的電腦和手機个少,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來眯杏,“玉大人夜焦,你說我怎么就攤上這事∑穹罚” “怎么了茫经?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長萎津。 經(jīng)常有香客問我卸伞,道長,這世上最難降的妖魔是什么锉屈? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任荤傲,我火速辦了婚禮,結(jié)果婚禮上颈渊,老公的妹妹穿的比我還像新娘遂黍。我一直安慰自己,他們只是感情好儡炼,可當我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布妓湘。 她就那樣靜靜地躺著查蓉,像睡著了一般乌询。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上豌研,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天妹田,我揣著相機與錄音唬党,去河邊找鬼。 笑死鬼佣,一個胖子當著我的面吹牛驶拱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晶衷,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼蓝纲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晌纫?” 一聲冷哼從身側(cè)響起税迷,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锹漱,沒想到半個月后箭养,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡哥牍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年毕泌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗅辣。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡撼泛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澡谭,到底是詐尸還是另有隱情坎弯,我是刑警寧澤,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布译暂,位于F島的核電站抠忘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏外永。R本人自食惡果不足惜崎脉,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伯顶。 院中可真熱鬧囚灼,春花似錦、人聲如沸祭衩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掐暮。三九已至蝎抽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間路克,已是汗流浹背樟结。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工养交, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓢宦。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓碎连,卻偏偏與公主長得像,于是被迫代替她去往敵國和親驮履。 傳聞我的和親對象是個殘疾皇子鱼辙,可洞房花燭夜當晚...
    茶點故事閱讀 43,587評論 2 350

推薦閱讀更多精彩內(nèi)容