前言
原型,作為前端開(kāi)發(fā)者,或多或少都有聽(tīng)說(shuō)。你可能一直想了解它,但是由于各種原因還沒(méi)有了解,現(xiàn)在就跟隨我來(lái)一起探索它吧。本文將由淺入深,一點(diǎn)一點(diǎn)揭開(kāi) JavaScript 原型的神秘面紗。(需要了解基本的 JavaScript 對(duì)象知識(shí))
源代碼:GitHub
原型
1. 原型是什么?
在我們深入探索之前,當(dāng)然要先了解原型是什么了,不然一切都無(wú)從談起。談起原型,那得先從對(duì)象說(shuō)起,且讓我們慢慢說(shuō)起。
我們都知道,JavaScript 是一門(mén)基于對(duì)象的腳本語(yǔ)言,但是它卻沒(méi)有類(lèi)的概念,所以 JavaScript 中的對(duì)象和基于類(lèi)的語(yǔ)言(如 Java)中的對(duì)象有所不同。JavaScript 中的對(duì)象是無(wú)序?qū)傩缘募?,其屬性可以包含基本值,?duì)象或者函數(shù),聽(tīng)起來(lái)更像是鍵值對(duì)的集合,事實(shí)上也比較類(lèi)似。有了對(duì)象,按理說(shuō)得有繼承,不然對(duì)象之間沒(méi)有任何聯(lián)系,也就真淪為鍵值對(duì)的集合了。那沒(méi)有類(lèi)的 JavaScript 是怎么實(shí)現(xiàn)繼承的呢?
我們知道,在 JavaScript 中可以使用構(gòu)造函數(shù)語(yǔ)法(通過(guò) new 調(diào)用的函數(shù)通常被稱(chēng)為構(gòu)造函數(shù))來(lái)創(chuàng)建一個(gè)新的對(duì)象,像下面這樣:
// 構(gòu)造函數(shù),無(wú)返回值
function Person(name) {
this.name = name;
}
// 通過(guò) new 新建一個(gè)對(duì)象
var person = new Person('Mike');
這和一般面向?qū)ο缶幊陶Z(yǔ)言中創(chuàng)建對(duì)象(Java 或 C++)的語(yǔ)法很類(lèi)似,只不過(guò)是一種簡(jiǎn)化的設(shè)計(jì),new 后面跟的不是類(lèi),而是構(gòu)造函數(shù)。這里的構(gòu)造函數(shù)可以看做是一種類(lèi)型,就像面向?qū)ο缶幊陶Z(yǔ)言中的類(lèi),但是這樣創(chuàng)建的對(duì)象除了屬性一樣外,并沒(méi)有其他的任何聯(lián)系,對(duì)象之間無(wú)法共享屬性和方法。每當(dāng)我們新建一個(gè)對(duì)象時(shí),都會(huì)方法和屬性分配一塊新的內(nèi)存,這是極大的資源浪費(fèi)。考慮到這一點(diǎn),JavaScript 的設(shè)計(jì)者 Brendan Eich 決定為構(gòu)造函數(shù)設(shè)置一個(gè)屬性。這個(gè)屬性指向一個(gè)對(duì)象,所有實(shí)例對(duì)象需要共享的屬性和方法,都放在這個(gè)對(duì)象里面,那些不需要共享的屬性和方法,就放在構(gòu)造函數(shù)里面。實(shí)例對(duì)象一旦創(chuàng)建,將自動(dòng)引用這個(gè)對(duì)象的屬性和方法。也就是說(shuō),實(shí)例對(duì)象的屬性和方法,分成兩種,一種是本地的,不共享的,另一種是引用的,共享的。這個(gè)對(duì)象就是原型(prototype)對(duì)象,簡(jiǎn)稱(chēng)為原型。
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè) prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,這個(gè)對(duì)象就是調(diào)用構(gòu)造函數(shù)而創(chuàng)建的對(duì)象實(shí)例的原型。原型可以包含所有實(shí)例共享的屬性和方法,也就是說(shuō)只要是原型有的屬性和方法,通過(guò)調(diào)用構(gòu)造函數(shù)而生成的對(duì)象實(shí)例都會(huì)擁有這些屬性和方法??聪旅娴拇a:
function Person(name) {
this.name = name;
}
Person.prototype.age = '20';
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person('Jack');
var person2 = new Person('Mike');
person1.sayName(); // Jack
person2.sayName(); // Mike
console.log(person1.age); // 20
console.log(person2.age); // 20
這段代碼中我們聲明了一個(gè) Person 函數(shù),并在這個(gè)函數(shù)的原型上添加了 age 屬性和 sayName 方法,然后生成了兩個(gè)對(duì)象實(shí)例 person1 和 person2,這兩個(gè)實(shí)例分別擁有自己的屬性 name 和原型的屬性 age 以及方法 sayName。所有的實(shí)例對(duì)象共享原型對(duì)象的屬性和方法,那么看起來(lái),原型對(duì)象就像是類(lèi),我們就可以用原型來(lái)實(shí)現(xiàn)繼承了。
2. constructor 與 [[Prototype]]
我們知道每個(gè)函數(shù)都有一個(gè) prototype 屬性,指向函數(shù)的原型,因此當(dāng)我們拿到一個(gè)函數(shù)的時(shí)候,就可以確定函數(shù)的原型。反之,如果給我們一個(gè)函數(shù)的原型,我們?cè)趺粗肋@個(gè)原型是屬于哪個(gè)函數(shù)的呢?這就要說(shuō)說(shuō)原型的 constructor 屬性了:
在默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè) constructor (構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向 prototype 屬性所在函數(shù)的指針。
也就是說(shuō)每個(gè)原型都有都有一個(gè) constructor 屬性,指向了原型所在的函數(shù),拿前面的例子來(lái)說(shuō) Person.prototype.constructor 指向 Person。下面是構(gòu)造函數(shù)和原型的關(guān)系說(shuō)明圖:
繼續(xù),讓我們說(shuō)說(shuō) [[prototype]]。
當(dāng)我們調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新的實(shí)例(新的對(duì)象)之后,比如上面例子中的 person1,實(shí)例的內(nèi)部會(huì)包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型。ECMA-262 第 5 版中管這個(gè)指針叫[[Prototype]]。我們可與更新函數(shù)和原型的關(guān)系圖:
不過(guò)在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn) [[Prototype]] , 但在 Firefox、Safari 和 Chrome 中可以通過(guò) __proto__屬性訪問(wèn)。而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。不過(guò),要明確的真正重要的一點(diǎn)就是,這個(gè)連接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。
在 VSCode 中開(kāi)啟調(diào)試模式,我們可以看到這些關(guān)系:
從上圖中我們可以看到 Person 的 prototype 屬性和 person1 的 __proto__ 屬性是完全一致的,Person.prototype 包含了一個(gè) constructor 屬性,指向了 Person 函數(shù)。這些可以很好的印證我們上面所說(shuō)的構(gòu)造函數(shù)、原型、constructor 以及 __proto__ 之間的關(guān)系。
3. 對(duì)象實(shí)例與原型
了解完構(gòu)造函數(shù),原型,對(duì)象實(shí)例之間的關(guān)系后,下面我們來(lái)深入探討一下對(duì)象和原型之間的關(guān)系。
1. 判斷對(duì)象實(shí)例和原型之間的關(guān)系
因?yàn)槲覀儫o(wú)法直接訪問(wèn)實(shí)例對(duì)象的 __proto__ 屬性,所以當(dāng)我們想要確定一個(gè)對(duì)象實(shí)例和某個(gè)原型之間是否存在關(guān)系時(shí),可能會(huì)有些困難,好在我們有一些方法可以判斷。
我們可以通過(guò) isPrototypeOf() 方法判斷某個(gè)原型和對(duì)象實(shí)例是否存在關(guān)系,或者,我們也可以使用 ES5 新增的方法 Object.getPrototypeOf() 獲取一個(gè)對(duì)象實(shí)例 __proto__ 屬性的值??聪旅娴睦樱?/p>
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
2. 對(duì)象實(shí)例屬性和方法的獲取
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先從對(duì)象實(shí)例本身開(kāi)始。如果在實(shí)例對(duì)象中找到了具有給定名字的屬性,則返回該屬性的值。如果沒(méi)有找到,則繼續(xù)搜索 __proto__ 指針指向的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性,如果在原型對(duì)象中找到了這個(gè)屬性,則返回該屬性的值。如果還找不到,就會(huì)接著查找原型的原型,直到最頂層為止。這正是多個(gè)對(duì)象實(shí)例共享原型所保存的屬性和方法的基本原理。
雖然可以通過(guò)對(duì)象實(shí)例訪問(wèn)保存在原型中的值,但卻不能通過(guò)對(duì)象實(shí)例重寫(xiě)原型中的值。我們?cè)趯?shí)例中添加的一個(gè)屬性,會(huì)屏蔽原型中的同名屬性。另外,通過(guò) hasOwnProperty 方法能判斷對(duì)象實(shí)例中是否存在某個(gè)屬性(不能判斷對(duì)象原型中是否存在該屬性)。來(lái)看下面的例子:
function Person(){ }
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){ console.log(this.name); };
var person1 = new Person();
var person2 = new Person();
// 注意,此處不能用 name,因?yàn)楹瘮?shù)本身存在 name 屬性
console.log(person1.hasOwnProperty('age')); // false
console.log(Person.hasOwnProperty('age')); // false
person1.name = 'Greg';
console.log(person1.hasOwnProperty('name')); // true
console.log(person1.name); //"Greg"——來(lái)自實(shí)例
console.log(person2.name); //"Nicholas"——來(lái)自原型
3. in 操作符
有兩種方式使用 in 操作符:
-
單獨(dú)使用
在單獨(dú)使用時(shí),in 操作符會(huì)在通過(guò)對(duì)象能夠訪問(wèn)給定屬性時(shí)返回 true,無(wú)論該屬性存在于實(shí)例中還是原型中。
-
for-in 循環(huán)中使用。
在使用 for-in 循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪問(wèn)的、可枚舉的(enumerated)屬性,其中既包括存在于實(shí)例中的屬性, 也包括存在于原型中的屬性。如果需要獲取所有的屬性(包括不可枚舉的屬性),可以使用 Object.getOwnPropertyNames() 方法。
看下面的例子:
function Person(){
this.name = 'Mike';
}
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){ console.log(this.name); };
var person = new Person();
for(var item in person) {
console.log(item); // name age job sayName
}
console.log('name' in person); // true - 來(lái)自實(shí)例
console.log('age' in person); // true - 來(lái)自原型
4. 原型的動(dòng)態(tài)性
由于在對(duì)象中查找屬性的過(guò)程是一次搜索,而實(shí)例與原型之間的連接只不過(guò)是一個(gè)指針,而非一個(gè)副本,因此我們對(duì)原型對(duì)象所做的任何修改都能夠立即從實(shí)例上反映出來(lái)——即使是先創(chuàng)建了實(shí)例后修改原型也照樣如此:
var person = new Person();
Person.prototype.sayHi = function(){ console.log("hi"); };
person.sayHi(); // "hi"
上面的代碼中,先創(chuàng)建了 Person 的一個(gè)實(shí)例,并將其保存在 person 中。然后,下一條語(yǔ)句在 Person.prototype 中添加了一個(gè)方法 sayHi()。即使 person 實(shí)例是在添加新方法之前創(chuàng)建的,但它仍然可以訪問(wèn)這個(gè)新方法。在調(diào)用這個(gè)方法時(shí),首先會(huì)查找 person 實(shí)例中是否有這個(gè)方法,發(fā)現(xiàn)沒(méi)有,然后到 person 的原型對(duì)象中查找,原型中存在這個(gè)方法,查找結(jié)束。;
但是下面這種代碼所得到的結(jié)果就完全不一樣了:
function Person() {}
var person = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
}
};
person.sayName(); // error
仔細(xì)觀察上面的代碼,我們直接用對(duì)象字面量語(yǔ)法給 Person.prototype 賦值,這似乎沒(méi)有什么問(wèn)題。但是我們要知道字面量語(yǔ)法會(huì)生成一個(gè)新的對(duì)象,也就是說(shuō)這里的 Person.prototype 是一個(gè)新的對(duì)象,和 person 的 __proto__ 屬性不再有任何關(guān)系了。此時(shí),我們?cè)賴(lài)L試調(diào)用 sayName 方法就會(huì)報(bào)錯(cuò),因?yàn)?person 的 __proto__ 屬性指向的還是原來(lái)的原型對(duì)象,而原來(lái)的原型對(duì)象上并沒(méi)有 sayName 方法,所以就會(huì)報(bào)錯(cuò)。
原型鏈
1. 原型的原型
在前面的例子,我們是直接在原型上添加屬性和方法,或者用一個(gè)新的對(duì)象賦值給原型,那么如果我們讓原型對(duì)象等于另一個(gè)類(lèi)型的實(shí)例,結(jié)果會(huì)怎樣呢?
function Person() {
this.age = '20';
}
Person.prototype.weight = '120';
function Engineer() {
this.work = 'Front-End';
}
Engineer.prototype = new Person();
Engineer.prototype.getAge = function() {
console.log(this.age);
}
var person = new Person();
var engineer = new Engineer();
console.log(person.age); // 20
engineer.getAge(); // 20
console.log(engineer.weight); // 120
console.log(Engineer.prototype.__proto__ == Person.prototype); // true
在上面代碼中,有兩個(gè)構(gòu)造函數(shù) Person 和 Engineer,可以看做是兩個(gè)類(lèi)型,Engineer 的原型是 Person 的一個(gè)實(shí)例,也就是說(shuō) Engineer 的原型指向了 Person 的原型(注意上面的最后一行代碼)。然后我們分別新建一個(gè) Person 和 Engineer 的實(shí)例對(duì)象,可以看到 engineer 實(shí)例對(duì)象能夠訪問(wèn)到 Person 的 age 和 weight 屬性,這很好理解:Engineer 的原型是 Person 的實(shí)例對(duì)象,Person 的實(shí)例對(duì)象包含了 age 屬性,而 weight 屬性是 Person 原型對(duì)象的屬性,Person 的實(shí)例對(duì)象自然可以訪問(wèn)原型中的屬性,同理,Engineer 的實(shí)例對(duì)象 engineer 也能訪問(wèn) Engineer 原型上的屬性,間接的也能訪問(wèn) Person 原型的屬性。
看起來(lái)關(guān)系有些復(fù)雜,不要緊,我們用一張圖片來(lái)解釋這些關(guān)系:
是不是一下就很清楚了,順著圖中紅色的線(xiàn),engineer 實(shí)例對(duì)象可以順利的獲取 Person 實(shí)例的屬性以及 Person 原型的屬性。至此,已經(jīng)鋪墊的差不多了,我們理解了原型的原型之后,也就很容易理解原型鏈了。
2. 原型鏈
原型鏈其實(shí)不難理解,上圖中的紅色線(xiàn)組成的鏈就可以稱(chēng)之為原型鏈,只不過(guò)這是一個(gè)不完整的原型鏈。我們可以這樣定義原型鏈:
原型對(duì)象可以包含一個(gè)指向另一個(gè)原型(原型2)的指針,相應(yīng)地,另一個(gè)原型(原型2)中也可以包含著一個(gè)指向?qū)?yīng)構(gòu)造函數(shù)(原型2 的構(gòu)造函數(shù))的指針。假如另一個(gè)原型(原型2)又是另一個(gè)類(lèi)型(原型3 的構(gòu)造函數(shù))的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
結(jié)合上面的圖,這個(gè)概念不難理解。上面的圖中只有兩個(gè)原型,那么當(dāng)有更多的原型之后,這個(gè)紅色的線(xiàn)理論上可以無(wú)限延伸,也就構(gòu)成了原型鏈。
通過(guò)實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了前面提到過(guò)的原型搜索機(jī)制:當(dāng)以讀取模式訪問(wèn)一個(gè)實(shí)例的屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒(méi)有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過(guò)原型鏈實(shí)現(xiàn)繼承的情況下,搜索過(guò)程就得以沿著原型鏈繼續(xù)向上。在找不到屬性或方法的情況下,搜索過(guò)程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來(lái)。
那么原型鏈的末端又是什么呢?我們要知道,所有函數(shù)的 默認(rèn)原型 都是 Object 的實(shí)例,因此默認(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指向 Object.prototype。我們可以在上面代碼的尾部加上一行代碼進(jìn)行驗(yàn)證:
console.log(Person.prototype.__proto__ == Object.prototype); // true
那 Object.prototype 的原型又是什么呢,不可能沒(méi)有終點(diǎn)???聰明的小伙伴可能已經(jīng)猜到了,沒(méi)錯(cuò),就是 null,null 表示此處不應(yīng)該有值,也就是終點(diǎn)了。我們可以在 Chrome 的控制臺(tái)或 Node 中驗(yàn)證一下:
console.log(Object.prototype.__proto__); // null
我們更新一下關(guān)系圖:
至此,一切已經(jīng)很清楚了,下面我們來(lái)說(shuō)說(shuō)原型鏈的用處。
繼承
繼承是面向?qū)ο笳Z(yǔ)言中的一個(gè)很常見(jiàn)的概念,在閱讀前面代碼的過(guò)程中,我們其實(shí)已經(jīng)實(shí)現(xiàn)了簡(jiǎn)單的繼承關(guān)系,細(xì)心的小伙伴可能已經(jīng)發(fā)現(xiàn)了。在 JavaScript 中,實(shí)現(xiàn)繼承主要是依靠原型鏈來(lái)實(shí)現(xiàn)的。
1. 原型鏈實(shí)現(xiàn)
一個(gè)簡(jiǎn)的基于原型鏈的繼承實(shí)現(xiàn)看起來(lái)是這樣的:
// 父類(lèi)型
function Super(){
this.flag = 'super';
}
Super.prototype.getFlag = function(){
return this.flag;
}
// 子類(lèi)型
function Sub(){
this.subFlag = 'sub';
}
// 實(shí)現(xiàn)繼承
Sub.prototype = new Super();
Sub.prototype.getSubFlag = function(){
return this.subFlag;
}
var instance = new Sub();
console.log(instance.subFlag); // sub
console.log(instance.flag); // super
原型鏈雖然很強(qiáng)大,可以實(shí)現(xiàn)繼承,但是會(huì)存在一些問(wèn)題:
引用類(lèi)型的原型屬性會(huì)被所有實(shí)例共享。
在通過(guò)原型鏈來(lái)實(shí)現(xiàn)繼承時(shí),引用類(lèi)型的屬性被會(huì)所有實(shí)例共享,一旦一個(gè)實(shí)例修改了引用類(lèi)型的值,會(huì)立刻反應(yīng)到其他實(shí)例上。由于基本類(lèi)型不是共享的,所以彼此不會(huì)影響。創(chuàng)建子類(lèi)型的實(shí)例時(shí),不能向父類(lèi)型的構(gòu)造函數(shù)傳遞參數(shù)。
實(shí)際上,應(yīng)該說(shuō)是沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下,給父類(lèi)型的構(gòu)造函數(shù)傳遞參數(shù),我們傳遞的參數(shù)會(huì)成為所有實(shí)例的屬性。
基于上面兩個(gè)問(wèn)題,實(shí)踐中很少單獨(dú)使用原型鏈實(shí)現(xiàn)繼承。
2. 借用構(gòu)造函數(shù)
為了解決上面出現(xiàn)的問(wèn)題,出現(xiàn)了一種叫做 借用構(gòu)造函數(shù)的技術(shù)。這種技術(shù)的基本思想很簡(jiǎn)單:apply() 或 call() 方法,在子類(lèi)型構(gòu)造函數(shù)的內(nèi)部調(diào)用父類(lèi)型的構(gòu)造函數(shù),使得子類(lèi)型擁有父類(lèi)型的屬性和方法。
function Super(properties){
this.properties = [].concat(properties);
this.colors = ['red', 'blue', 'green'];
}
function Sub(properties){
// 繼承了 Super,傳遞參數(shù),互不影響
Super.apply(this, properties);
}
var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red, blue, green, black'
console.log(instance1.properties[0]); // 'instance1'
var instance2 = new Sub();
console.log(instance2.colors); // 'red, blue, green'
console.log(instance2.properties[0]); // 'undefined'
借用構(gòu)造函數(shù)的確可以解決上面提到的兩個(gè)問(wèn)題,實(shí)例間不會(huì)共享屬性,也可以向父類(lèi)型傳遞參數(shù),但是這種方法任然存在一些問(wèn)題:子類(lèi)型無(wú)法繼承父類(lèi)型原型中的屬性。我們只在子類(lèi)型的構(gòu)造函數(shù)中調(diào)用了父類(lèi)型的構(gòu)造函數(shù),沒(méi)有做其他的,子類(lèi)型和父類(lèi)型的原型也就沒(méi)有任何聯(lián)系。考慮到這個(gè)問(wèn)題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。
3. 組合繼承
上面兩個(gè)方法能夠互補(bǔ)彼此的不足之處,我們把這兩個(gè)方法結(jié)合起來(lái),就能比較完美的解決問(wèn)題了,這就是組合繼承。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性,從而發(fā)揮二者之長(zhǎng)??匆粋€(gè)簡(jiǎn)單的實(shí)現(xiàn):
function Super(properties){
this.properties = [].concat(properties);
this.colors = ['red', 'blue', 'green'];
}
Super.prototype.log = function() {
console.log(this.properties[0]);
}
function Sub(properties){
// 繼承了 Super,傳遞參數(shù),互不影響
Super.apply(this, properties);
}
// 繼承了父類(lèi)型的原型
Sub.prototype = new Super();
// isPrototypeOf() 和 instance 能正常使用
Sub.prototype.construnctor = Sub;
var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red,blue,green,black'
instance1.log(); // 'instance1'
var instance2 = new Sub();
console.log(instance2.colors); // 'red,blue,green'
instance2.log(); // 'undefined'
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),是 JavaScript 中最常用的繼承模式。組合繼承看起來(lái)很不錯(cuò),但是也有它的缺點(diǎn):無(wú)論什么情況下,組合繼承都會(huì)調(diào)用兩次父類(lèi)型的構(gòu)造函數(shù):一次是在創(chuàng)建子類(lèi)型原型的時(shí)候,另一次是在子類(lèi)型構(gòu)造函數(shù)內(nèi)部。
4. 寄生組合式繼承
為了解決上面組合繼承的問(wèn)題,一種新的繼承方式出現(xiàn)了-寄生組合繼承,可以說(shuō)是 JavaScript 中繼承最理想的解決方案。
// 用于繼承的函數(shù)
function inheritPrototype(child, parent) {
var F = function () {}
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
}
// 父類(lèi)型
function Super(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Super.prototype.sayName = function () {
console.log(this.name);
};
// 子類(lèi)型
function Sub(name, age) {
// 繼承基本屬性和方法
SuperType.call(this, name);
this.age = age;
}
// 繼承原型上的屬性和方法
inheritPrototype(Sub, Spuer);
Sub.prototype.log = function () {
console.log(this.age);
};
所謂寄生組合式繼承,即通過(guò)借用構(gòu)造函數(shù)來(lái)繼承屬性,通過(guò)借用臨時(shí)構(gòu)造函數(shù)來(lái)繼承原型。其背后的基本思路是:不必為了指定子類(lèi)型的原型而調(diào)用父類(lèi)型的構(gòu)造函數(shù),我們所需要的無(wú)非就是父類(lèi)型原型的一個(gè)副本而已。
參考
- 《JavaScript 高級(jí)程序設(shè)計(jì)》
- Javascript繼承機(jī)制的設(shè)計(jì)思想