對象、類與面向?qū)ο缶幊?/h1>

繼承

繼承是面向?qū)ο缶幊讨杏懻撟疃嗟脑掝}匣砖。很多面向?qū)ο笳Z言都支持兩種繼承:接口繼承和實現(xiàn)繼承沾鳄。
前者只繼承方法簽名淆储,后者繼承實際的方法潭千。接口繼承在 ECMAScript 中是不可能的躏率,因為函數(shù)沒有簽
名怀泊。實現(xiàn)繼承是 ECMAScript 唯一支持的繼承方式茫藏,而這主要是通過原型鏈實現(xiàn)的。

原型鏈

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()方法轴或。
圖 8-4 展示了子類的實例與兩個構(gòu)造函數(shù)及其對應(yīng)的原型之間的關(guān)系。



這個例子中實現(xiàn)繼承的關(guān)鍵仰禀,是 SubType 沒有使用默認(rèn)原型照雁,而是將其替換成了一個新的對象。這個
新的對象恰好是 SuperType 的實例答恶。這樣一來饺蚊,SubType 的實例不僅能從 SuperType 的實例中繼承屬性
和方法,而且還與 SuperType 的原型掛上了鉤悬嗓。于是 instance(通過內(nèi)部的[[Prototype]])指向
SubType.prototype污呼,而 SubType.prototype(作為 SuperType 的實例又通過內(nèi)部的[[Prototype]])
指向 SuperType.prototype。注意包竹,getSuperValue()方法還在 SuperType.prototype 對象上燕酷,
而 property 屬性則在 SubType.prototype 上。這是因為 getSuperValue()是一個原型方法周瞎,而
property 是一個實例屬性苗缩。SubType.prototype 現(xiàn)在是 SuperType 的一個實例,因此 property
才會存儲在它上面声诸。還要注意酱讶,由于 SubType.prototype 的 constructor 屬性被重寫為指向
SuperType,所以 instance.constructor 也指向 SuperType彼乌。
原型鏈擴展了前面描述的原型搜索機制泻肯。我們知道,在讀取實例上的屬性時慰照,首先會在實例上搜索
這個屬性灶挟。如果沒找到,則會繼承搜索實例的原型毒租。在通過原型鏈實現(xiàn)繼承之后膏萧,搜索就可以繼承向上,
搜索原型的原型蝌衔。對前面的例子而言,調(diào)用 instance.getSuperValue()經(jīng)過了 3 步搜索:instance蝌蹂、
SubType.prototype 和 SuperType.prototype噩斟,最后一步才找到這個方法。對屬性和方法的搜索會
一直持續(xù)到原型鏈的末端孤个。

  • 默認(rèn)原型
    實際上剃允,原型鏈中還有一環(huán)。默認(rèn)情況下,所有引用類型都繼承自 Object斥废,這也是通過原型鏈實
    現(xiàn)的椒楣。任何函數(shù)的默認(rèn)原型都是一個 Object 的實例,這意味著這個實例有一個內(nèi)部指針指向
    Object.prototype牡肉。這也是為什么自定義類型能夠繼承包括 toString()捧灰、valueOf()在內(nèi)的所有默
    認(rèn)方法的原因。因此前面的例子還有額外一層繼承關(guān)系统锤。圖 8-5 展示了完整的原型鏈毛俏。

    SubType 繼承 SuperType,而 SuperType 繼承 Object饲窿。在調(diào)用 instance.toString()時煌寇,實
    際上調(diào)用的是保存在 Object.prototype 上的方法。
  • 原型與繼承關(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
  • 關(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

在上面的代碼中,涉及兩個方法帖池。第一個方法 getSubValue()是 SubType 的新方法奈惑,
而第二個方法 getSuperValue()是原型鏈上已經(jīng)存在但在這里被遮蔽的方法。后面在 SubType 實例
上調(diào)用 getSuperValue()時調(diào)用的是這個方法睡汹。而 SuperType 的實例仍然會調(diào)用最初的方法肴甸。重點
在于上述兩個方法都是在把原型賦值為 SuperType 的實例之后定義的。
另一個要理解的重點是囚巴,以對象字面量方式創(chuàng)建原型方法會破壞之前的原型鏈原在,因為這相當(dāng)于重寫
了原型鏈友扰。下面是一個例子:

 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
}
// 繼承 SuperType 
SubType.prototype = new SuperType(); 
// 通過對象字面量添加新方法,這會導(dǎo)致上一行無效
SubType.prototype = { 
 getSubValue() { 
 return this.subproperty; 
 }, 
 someOtherMethod() { 
 return false; 
 } 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // 出錯庶柿!

在這段代碼中村怪,子類的原型在被賦值為 SuperType 的實例后,又被一個對象字面量覆蓋了浮庐。覆蓋
后的原型是一個 Object 的實例甚负,而不再是 SuperType 的實例。因此之前的原型鏈就斷了兔辅。SubType
和 SuperType 之間也沒有關(guān)系了腊敲。

  • 原型鏈的問題
    原型鏈雖然是實現(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"

在這個例子中,SuperType 構(gòu)造函數(shù)定義了一個 colors 屬性褐澎,其中包含一個數(shù)組(引用值)会钝。每
個 SuperType 的實例都會有自己的 colors 屬性,包含自己的數(shù)組工三。但是迁酸,當(dāng) SubType 通過原型繼承
SuperType 后,SubType.prototype 變成了 SuperType 的一個實例俭正,因而也獲得了自己的 colors
屬性奸鬓。這類似于創(chuàng)建了 SubType.prototype.colors 屬性。最終結(jié)果是掸读,SubType 的所有實例都會
共享這個 colors 屬性串远。這一點通過 instance1.colors 上的修改也能反映到 instance2.colors
上就可以看出來。
原型鏈的第二個問題是儿惫,子類型在實例化時不能給父類型的構(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ù)。來看下面的例子:

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í)行了。這相當(dāng)于新的 SubType 對象上運行了
SuperType()函數(shù)中的所有初始化代碼账锹。結(jié)果就是每個實例都會有自己的 colors 屬性萌业。

  • 傳遞參數(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

在這個例子中生年,SuperType 構(gòu)造函數(shù)接收一個參數(shù) name,然后將它賦值給一個屬性廓奕。在 SubType
構(gòu)造函數(shù)中調(diào)用 SuperType 構(gòu)造函數(shù)時傳入這個參數(shù)抱婉,實際上會在 SubType 的實例上定義 name 屬性。
為確保 SuperType 構(gòu)造函數(shù)不會覆蓋 SubType 定義的屬性桌粉,可以在調(diào)用父類構(gòu)造函數(shù)之后再給子類實
例添加額外的屬性蒸绩。

  • 盜用構(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

在這個例子中,SuperType 構(gòu)造函數(shù)定義了兩個屬性士骤,name 和 colors范删,而它的原型上也定義了
一個方法叫 sayName()。SubType 構(gòu)造函數(shù)調(diào)用了 SuperType 構(gòu)造函數(shù)拷肌,傳入了 name 參數(shù)到旦,然后又
定義了自己的屬性 age旨巷。此外,SubType.prototype 也被賦值為 SuperType 的實例添忘。原型賦值之后采呐,
又在這個原型上添加了新方法 sayAge()。這樣搁骑,就可以創(chuàng)建兩個 SubType 實例斧吐,讓這兩個實例都有
自己的屬性,包括 colors仲器,同時還共享相同的方法煤率。
組合繼承彌補了原型鏈和盜用構(gòu)造函數(shù)的不足,是 JavaScript 中使用最多的繼承模式乏冀。而且組合繼
承也保留了 instanceof 操作符和 isPrototypeOf()方法識別合成對象的能力蝶糯。

原型式繼承

2006 年,Douglas Crockford 寫了一篇文章:《JavaScript 中的原型式繼承》(“Prototypal Inheritance in
JavaScript”)煤辨。這篇文章介紹了一種不涉及嚴(yán)格意義上構(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()团赁,然后再對返回的對象進行適當(dāng)修改。在這個例子中谨履,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ù),就是新對象的基準(zhǔn)對象鞠眉。這個對象 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ù)都可以在這里使用胆胰。

寄生式組合繼承

組合繼承其實也存在效率問題狞贱。最主要的效率問題就是父類構(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簿透。這兩個實例屬性會遮蔽原型上同名的屬性。圖 8-6 展示了這個過程解藻。



8-6

如圖 8-6 所示老充,有兩組 name 和 colors 屬性:一組在實例上,另一組在 SubType 的原型上螟左。這是
調(diào)用兩次 SuperType 構(gòu)造函數(shù)的結(jié)果啡浊。好在有辦法解決這個問題。
寄生式組合繼承通過盜用構(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)致默認(rèn) 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)
趾断,因此剛開始接觸時可能會不太習(xí)慣拒名。雖然 ECMAScript 6 類表面
上看起來可以支持正式的面向?qū)ο缶幊蹋珜嶋H上它背后使用的仍然是原型和構(gòu)造函數(shù)的概念
芋酌。

類定義

與函數(shù)類型相似增显,定義類也有兩種主要方式:類聲明和類表達(dá)式。這兩種方式都使用 class 關(guān)鍵
字加大括號:

// 類聲明
class Person {} 
// 類表達(dá)式
const Animal = class {};

與函數(shù)表達(dá)式類似脐帝,類表達(dá)式在它們被求值前也不能引用同云。不過,與函數(shù)定義不同的是堵腹,雖然函數(shù)
聲明可以提升梢杭,但類定義不能:

console.log(FunctionExpression); // undefined 
var FunctionExpression = function() {}; 
console.log(FunctionExpression); // function() {} 

console.log(FunctionDeclaration); // FunctionDeclaration() {} 
function FunctionDeclaration() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 

console.log(ClassExpression); // undefined 
var ClassExpression = class {}; 
console.log(ClassExpression); // class {} 

console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 
class ClassDeclaration {} 
console.log(ClassDeclaration); // class ClassDeclaration {}

另一個跟函數(shù)聲明不同的地方是,函數(shù)受函數(shù)作用域限制秸滴,而類受塊作用域限制:

{ 
 function FunctionDeclaration() {} 
 class ClassDeclaration {} 
} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
  • 類的構(gòu)成
    類可以包含構(gòu)造函數(shù)方法、實例方法募判、獲取函數(shù)荡含、設(shè)置函數(shù)和靜態(tài)類方法,但這些都不是必需的届垫。
    空的類定義照樣有效释液。默認(rèn)情況下,類定義中的代碼都在嚴(yán)格模式下執(zhí)行装处。
    與函數(shù)構(gòu)造函數(shù)一樣误债,多數(shù)編程風(fēng)格都建議類名的首字母要大寫,以區(qū)別于通過它創(chuàng)建的實例(比
    如妄迁,通過 class Foo {}創(chuàng)建實例 foo):
// 空類定義寝蹈,有效 
class Foo {} 
// 有構(gòu)造函數(shù)的類,有效
class Bar { 
 constructor() {} 
} 
// 有獲取函數(shù)的類登淘,有效
class Baz { 
 get myBaz() {} 
} 
// 有靜態(tài)方法的類箫老,有效
class Qux { 
 static myQux() {} 
}

類表達(dá)式的名稱是可選的。在把類表達(dá)式賦值給變量后黔州,可以通過 name 屬性取得類表達(dá)式的名稱
字符串耍鬓。但不能在類表達(dá)式作用域外部訪問這個標(biāo)識符。

let Person = class PersonName { 
 identify() { 
 console.log(Person.name, PersonName.name); 
 } 
} 
let p = new Person(); 
p.identify(); // PersonName PersonName 
console.log(Person.name); // PersonName 
console.log(PersonName); // ReferenceError: PersonName is not defined

類構(gòu)造函數(shù)

constructor 關(guān)鍵字用于在類定義塊內(nèi)部創(chuàng)建類的構(gòu)造函數(shù)流妻。方法名 constructor 會告訴解釋器
在使用 new 操作符創(chuàng)建類的新實例時牲蜀,應(yīng)該調(diào)用這個函數(shù)。構(gòu)造函數(shù)的定義不是必需的绅这,不定義構(gòu)造函
數(shù)相當(dāng)于將構(gòu)造函數(shù)定義為空函數(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

類實例化時傳入的參數(shù)會用作構(gòu)造函數(shù)的參數(shù)谨设。如果不需要參數(shù),則類名后面的括號也是可選的:

class Person { 
 constructor(name) { 
   console.log(arguments.length); 
   this.name = name || null; 
 } 
} 
let p1 = new Person; // 0 
console.log(p1.name); // null 
let p2 = new Person(); // 0 
console.log(p2.name); // null 
let p3 = new Person('Jake'); // 1 
console.log(p3.name); // Jake

默認(rèn)情況下缎浇,類構(gòu)造函數(shù)會在執(zhí)行之后返回 this 對象扎拣。構(gòu)造函數(shù)返回的對象會被用作實例化的對象,如果沒有什么引用新創(chuàng)建的 this 對象素跺,那么這個對象會被銷毀二蓝。不過,如果返回的不是 this 對象指厌,而是其他對象刊愚,那么這個對象不會通過 instanceof 操作符檢測出跟類有關(guān)聯(lián),因為這個對象的原型指針并沒有被修改踩验。

class Person { 
 constructor(override) { 
   this.foo = 'foo'; 
   if (override) { 
     return { 
       bar: 'bar' 
     }; 
   } 
 } 
} 
let p1 = new Person(), 
p2 = new Person(true); 
console.log(p1); // Person{ foo: 'foo' } 
console.log(p1 instanceof Person); // true 
console.log(p2); // { bar: 'bar' } 
console.log(p2 instanceof Person); // false

類構(gòu)造函數(shù)與構(gòu)造函數(shù)的主要區(qū)別是僧免,調(diào)用類構(gòu)造函數(shù)必須使用 new 操作符识啦。而普通構(gòu)造函數(shù)如果
不使用 new 調(diào)用书斜,那么就會以全局的 this(通常是 window)作為內(nèi)部對象伦连。調(diào)用類構(gòu)造函數(shù)時如果
忘了使用 new 則會拋出錯誤:

function Person() {} 
class Animal {} 
// 把 window 作為 this 來構(gòu)建實例
let p = Person(); 
let a = Animal(); 
// TypeError: class constructor Animal cannot be invoked without 'new'

類構(gòu)造函數(shù)沒有什么特殊之處,實例化之后袭异,它會成為普通的實例方法(但作為類構(gòu)造函數(shù)蓖捶,仍然要使用 new 調(diào)用)。因此扁远,實例化之后可以在實例上引用它:

class Person {} 
// 使用類創(chuàng)建一個新實例
let p1 = new Person(); 
p1.constructor(); 
// TypeError: Class constructor Person cannot be invoked without 'new' 
// 使用對類構(gòu)造函數(shù)的引用創(chuàng)建一個新實例
let p2 = new p1.constructor();
  • 2. 把類當(dāng)成特殊函數(shù)
    ECMAScript 中沒有正式的類這個類型俊鱼。從各方面來看,ECMAScript 類就是一種特殊函數(shù)畅买。聲明一個類之后并闲,通過 typeof 操作符檢測類標(biāo)識符,表明它是一個函數(shù):
class Person {} 
console.log(Person); // class Person {} 
console.log(typeof Person); // function

類標(biāo)識符有 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

由此可知溜徙,可以使用 instanceof 操作符檢查一個對象與類構(gòu)造函數(shù),以確定這個對象是不是類的
實例犀填。只不過此時的類構(gòu)造函數(shù)要使用類標(biāo)識符蠢壹,比如,在前面的例子中要檢查 p 和 Person九巡。
如前所述图贸,類本身具有與普通構(gòu)造函數(shù)一樣的行為。在類的上下文中冕广,類本身在使用 new 調(diào)用時就
會被當(dāng)成構(gòu)造函數(shù)疏日。重點在于,類中定義的 constructor 方法不會被當(dāng)成構(gòu)造函數(shù)撒汉,在對它使用
instanceof 操作符時會返回 false沟优。但是,如果在創(chuàng)建實例時直接將類構(gòu)造函數(shù)當(dāng)成普通構(gòu)造函數(shù)來
使用睬辐,那么 instanceof 操作符的返回值會反轉(zhuǎn):

class Person {} 
let p1 = new Person(); 
console.log(p1.constructor === Person); // true 
console.log(p1 instanceof Person); // true 
console.log(p1 instanceof Person.constructor); // false 
let p2 = new Person.constructor(); 
console.log(p2.constructor === Person); // false 
console.log(p2 instanceof Person); // false 
console.log(p2 instanceof Person.constructor); // true

類是 JavaScript 的一等公民挠阁,因此可以像其他對象或函數(shù)引用一樣把類作為參數(shù)傳遞:

let classList = [ 
 class { 
   constructor(id) { 
     this.id_ = id; 
     console.log(`instance ${this.id_}`); 
   } 
 } 
]; 
function createInstance(classDefinition, id) { 
 return new classDefinition(id); 
} 
let foo = createInstance(classList[0], 3141); // instance 3141

與立即調(diào)用函數(shù)表達(dá)式相似,類也可以立即實例化:

// 因為是一個類表達(dá)式溯饵,所以類名是可選的
let p = new class Foo {
 constructor(x) { 
   console.log(x); 
 } 
}('bar'); // bar 
console.log(p); // Foo {}

實例鹃唯、原型和類成員

類的語法可以非常方便地定義應(yīng)該存在于實例上的成員、應(yīng)該存在于原型上的成員瓣喊,以及應(yīng)該存在
于類本身的成員。

  • 1. 實例成員
    每次通過new調(diào)用類標(biāo)識符時黔酥,都會執(zhí)行類構(gòu)造函數(shù)藻三。在這個函數(shù)內(nèi)部,可以為新創(chuàng)建的實例(this)添加“自有”屬性跪者。至于添加什么樣的屬性棵帽,則沒有限制。另外渣玲,在構(gòu)造函數(shù)執(zhí)行完畢后逗概,仍然可以給實例繼續(xù)添加新成員。
    每個實例都對應(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

可以把方法定義在類構(gòu)造函數(shù)中或者類塊中枚钓,但不能在類塊中給原型添加原始值或?qū)ο笞鳛槌蓡T數(shù)據(jù):

class Person { 
 name: 'Jake' 
} 
// Uncaught SyntaxError: Unexpected token 

類方法等同于對象屬性铅搓,因此可以使用字符串、符號或計算的值作為鍵:

const symbolKey = Symbol('symbolKey'); 
class Person { 
 stringKey() { 
   console.log('invoked stringKey'); 
 } 
 [symbolKey]() { 
   console.log('invoked symbolKey'); 
 } 
 ['computed' + 'Key']() { 
   console.log('invoked computedKey'); 
 } 
} 
let p = new Person(); 
p.stringKey(); // invoked stringKey 
p[symbolKey](); // invoked symbolKey 
p.computedKey(); // invoked computedKey

類定義也支持獲取和設(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
  • 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

因為支持生成器方法晋渺,所以可以通過添加一個默認(rèn)的迭代器,把類實例變成可迭代對象:

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

派生類都會通過原型鏈訪問到類和原型上定義的方法重绷。this 的值會反映調(diào)用相應(yīng)方法的實例或者類:

class Vehicle { 
 identifyPrototype(id) { 
   console.log(id, this); 
 }
 static identifyClass(id) { 
   console.log(id, this); 
 } 
} 
class Bus extends Vehicle {} 
let v = new Vehicle(); 
let b = new Bus(); 
b.identifyPrototype('bus'); // bus, Bus {} 
v.identifyPrototype('vehicle'); // vehicle, Vehicle {} 
Bus.identifyClass('bus'); // bus, class Bus {} 
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}
  • 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(); // 相當(dāng)于 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
image.png

在使用 super 時要注意幾個問題。

  • super 只能在派生類構(gòu)造函數(shù)和靜態(tài)方法中使用倒淫。
class Vehicle { 
 constructor() { 
   super(); 
   // SyntaxError: 'super' keyword unexpected 
 } 
}
  • 不能單獨引用 super 關(guān)鍵字伙菊,要么用它調(diào)用構(gòu)造函數(shù),要么用它引用靜態(tài)方法敌土。
class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
   console.log(super); 
   // SyntaxError: 'super' keyword unexpected here 
 } 
}
  • 調(diào)用 super()會調(diào)用父類構(gòu)造函數(shù)镜硕,并將返回的實例賦值給 this。
class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
   super(); 
   console.log(this instanceof Vehicle); 
 } 
} 
new Bus(); // true
  • super()的行為如同調(diào)用構(gòu)造函數(shù)返干,如果需要給父類構(gòu)造函數(shù)傳參谦疾,則需要手動傳入。
class Vehicle { 
 constructor(licensePlate) { 
   this.licensePlate = licensePlate; 
 } 
} 
class Bus extends Vehicle { 
 constructor(licensePlate) { 
   super(licensePlate); 
 } 
} 
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  • 如果沒有定義類構(gòu)造函數(shù)犬金,在實例化派生類時會調(diào)用 super()念恍,而且會傳入所有傳給派生類的參數(shù)六剥。
class Vehicle { 
 constructor(licensePlate) { 
   this.licensePlate = licensePlate; 
 } 
} 
class Bus extends Vehicle {} 
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  • 在類構(gòu)造函數(shù)中,不能在調(diào)用 super()之前引用 this峰伙。
class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
   console.log(this); 
 } 
} 
new Bus(); 
// ReferenceError: Must call super constructor in derived class 
// before accessing 'this' or returning from derived constructor
  • 如果在派生類中顯式定義了構(gòu)造函數(shù)疗疟,則要么必須在其中調(diào)用 super(),要么必須在其中返回一個對象瞳氓。
class Vehicle {} 
class Car extends Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
   super(); 
 } 
} 
class Van extends Vehicle { 
 constructor() { 
   return {}; 
 } 
} 
console.log(new Car()); // Car {} 
console.log(new Bus()); // Bus {} 
console.log(new Van()); // {}
  • 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]

有些內(nèi)置類型的方法會返回新實例瞬场。默認(rèn)情況下,返回實例的類型與原始實例的類型是一致的:

class SuperArray extends Array {} 
let a1 = new SuperArray(1, 2, 3, 4, 5); 
let a2 = a1.filter(x => !!(x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // true

如果想覆蓋這個默認(rèn)行為涧郊,則可以覆蓋 Symbol.species 訪問器贯被,這個訪問器決定在創(chuàng)建返回的
實例時使用的類:

class SuperArray extends Array { 
 static get [Symbol.species]() { 
   return Array; 
 } 
} 
let a1 = new SuperArray(1, 2, 3, 4, 5); 
let a2 = a1.filter(x => !!(x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // false
  • 5. 類混入
    把不同類的行為集中到一個類是一種常見的 JavaScript 模式。雖然 ES6 沒有顯式支持多類繼承妆艘,但
    通過現(xiàn)有特性可以輕松地模擬這種行為彤灶。

    在下面的代碼片段中,extends 關(guān)鍵字后面是一個 JavaScript 表達(dá)式双仍。任何可以解析為一個類或一
    個構(gòu)造函數(shù)的表達(dá)式都是有效的。這個表達(dá)式會在求值類定義時被求值:
class Vehicle {} 
function getParentClass() { 
 console.log('evaluated expression'); 
 return Vehicle; 
} 
class Bus extends getParentClass() {} 
// 可求值的表達(dá)式

混入模式可以通過在一個表達(dá)式中連綴多個混入元素來實現(xiàn)桌吃,這個表達(dá)式最終會解析為一個可以被
繼承的類朱沃。如果 Person 類需要組合 A、B茅诱、C逗物,則需要某種機制實現(xiàn) B 繼承 A,C 繼承 B瑟俭,而 Person再繼承 C翎卓,從而把 A、B摆寄、C 組合到這個超類中失暴。實現(xiàn)這種模式有不同的策略坯门。
一個策略是定義一組“可嵌套”的函數(shù),每個函數(shù)分別接收一個超類作為參數(shù)逗扒,而將混入類定義為
這個參數(shù)的子類古戴,并返回這個類。這些組合函數(shù)可以連綴調(diào)用矩肩,最終組合成超類表達(dá)式:

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 

通過寫一個輔助函數(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'); 
 } 
}; 
function mix(BaseClass, ...Mixins) { 
 return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); 
} 
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz

小結(jié)

對象在代碼執(zhí)行過程中的任何時候都可以被創(chuàng)建和增強,具有極大的動態(tài)性黍檩,并不是嚴(yán)格定義的實
體叉袍。下面的模式適用于創(chuàng)建對象。

  • 工廠模式就是一個簡單的函數(shù)刽酱,這個函數(shù)可以創(chuàng)建對象喳逛,為它添加屬性和方法,然后返回這個
    對象肛跌。這個模式在構(gòu)造函數(shù)模式出現(xiàn)后就很少用了艺配。
  • 使用構(gòu)造函數(shù)模式可以自定義引用類型,可以使用 new 關(guān)鍵字像創(chuàng)建內(nèi)置類型實例一樣創(chuàng)建自
    定義類型的實例衍慎。不過转唉,構(gòu)造函數(shù)模式也有不足,主要是其成員無法重用稳捆,包括函數(shù)赠法。考慮到
    函數(shù)本身是松散的乔夯、弱類型的砖织,沒有理由讓函數(shù)不能在多個對象實例間共享。
  • 原型模式解決了成員共享的問題末荐,只要是添加到構(gòu)造函數(shù) prototype 上的屬性和方法就可以共
    享侧纯。而組合構(gòu)造函數(shù)和原型模式通過構(gòu)造函數(shù)定義實例屬性,通過原型定義共享的屬性和方法甲脏。

JavaScript 的繼承主要通過原型鏈來實現(xiàn)眶熬。原型鏈涉及把構(gòu)造函數(shù)的原型賦值為另一個類型的實例。
這樣一來块请,子類就可以訪問父類的所有屬性和方法娜氏,就像基于類的繼承那樣。原型鏈的問題是所有繼承
的屬性和方法都會在對象實例間共享墩新,無法做到實例私有贸弥。盜用構(gòu)造函數(shù)模式通過在子類構(gòu)造函數(shù)中調(diào)
用父類構(gòu)造函數(shù),可以避免這個問題海渊。這樣可以讓每個實例繼承的屬性都是私有的绵疲,但要求類型只能通
過構(gòu)造函數(shù)模式來定義(因為子類不能訪問父類原型上的方法)哲鸳。目前最流行的繼承模式是組合繼承,
即通過原型鏈繼承共享的屬性和方法最岗,通過盜用構(gòu)造函數(shù)繼承實例屬性帕胆。
除上述模式之外,還有以下幾種繼承模式般渡。

  • 原型式繼承可以無須明確定義構(gòu)造函數(shù)而實現(xiàn)繼承懒豹,本質(zhì)上是對給定對象執(zhí)行淺復(fù)制。這種操
    作的結(jié)果之后還可以再進一步增強驯用。
  • 與原型式繼承緊密相關(guān)的是寄生式繼承脸秽,即先基于一個對象創(chuàng)建一個新對象,然后再增強這個
    新對象蝴乔,最后返回新對象记餐。這個模式也被用在組合繼承中,用于避免重復(fù)調(diào)用父類構(gòu)造函數(shù)導(dǎo)
    致的浪費薇正。
  • 寄生組合繼承被認(rèn)為是實現(xiàn)基于類型繼承的最有效方式片酝。
    ECMAScript 6 新增的類很大程度上是基于既有原型機制的語法糖。類的語法讓開發(fā)者可以優(yōu)雅地定
    義向后兼容的類挖腰,既可以繼承內(nèi)置類型雕沿,也可以繼承自定義類型。類有效地跨越了對象實例猴仑、對象原型
    和對象類之間的鴻溝审轮。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者

  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辽俗,隨后出現(xiàn)的幾起案子疾渣,更是在濱河造成了極大的恐慌,老刑警劉巖崖飘,帶你破解...
    沈念sama閱讀 212,294評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榴捡,死亡現(xiàn)場離奇詭異,居然都是意外死亡朱浴,警方通過查閱死者的電腦和手機吊圾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赊琳,“玉大人街夭,你說我怎么就攤上這事砰碴□锓ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 157,790評論 0 348
  • 文/不壞的土叔 我叫張陵呈枉,是天一觀的道長趁尼。 經(jīng)常有香客問我埃碱,道長,這世上最難降的妖魔是什么酥泞? 我笑而不...
    開封第一講書人閱讀 56,595評論 1 284
  • 正文 為了忘掉前任砚殿,我火速辦了婚禮,結(jié)果婚禮上芝囤,老公的妹妹穿的比我還像新娘似炎。我一直安慰自己,他們只是感情好悯姊,可當(dāng)我...
    茶點故事閱讀 65,718評論 6 386
  • 文/花漫 我一把揭開白布羡藐。 她就那樣靜靜地躺著,像睡著了一般悯许。 火紅的嫁衣襯著肌膚如雪仆嗦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,906評論 1 290
  • 那天先壕,我揣著相機與錄音瘩扼,去河邊找鬼。 笑死垃僚,一個胖子當(dāng)著我的面吹牛集绰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冈在,決...
    沈念sama閱讀 39,053評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼倒慧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了包券?” 一聲冷哼從身側(cè)響起纫谅,我...
    開封第一講書人閱讀 37,797評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溅固,沒想到半個月后付秕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,250評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡侍郭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,570評論 2 327
  • 正文 我和宋清朗相戀三年询吴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亮元。...
    茶點故事閱讀 38,711評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡猛计,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出爆捞,到底是詐尸還是另有隱情奉瘤,我是刑警寧澤,帶...
    沈念sama閱讀 34,388評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站盗温,受9級特大地震影響藕赞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卖局,卻給世界環(huán)境...
    茶點故事閱讀 40,018評論 3 316
  • 文/蒙蒙 一斧蜕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砚偶,春花似錦批销、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,796評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至酒请,卻和暖如春骡技,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羞反。 一陣腳步聲響...
    開封第一講書人閱讀 32,023評論 1 266
  • 我被黑心中介騙來泰國打工布朦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昼窗。 一個月前我還...
    沈念sama閱讀 46,461評論 2 360
  • 正文 我出身青樓是趴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親澄惊。 傳聞我的和親對象是個殘疾皇子唆途,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,595評論 2 350

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