JavaScript的原型繼承詳解
JavaScript是一門面向?qū)ο蟮恼Z言。在JavaScript中有一句很經(jīng)典的話,萬物皆對(duì)象。既然是面向?qū)ο蟮?,那就有面向?qū)ο蟮娜筇卣鳎悍庋b、繼承、多態(tài)。這里講的是JavaScript的繼承,其他兩個(gè)容后再講。
JavaScript的繼承和C++的繼承不大一樣,C++的繼承是基于類的,而JavaScript的繼承是基于原型的。
現(xiàn)在問題來了。
原型是什么?原型我們可以參照C++里的類,同樣的保存了對(duì)象的屬性和方法。例如我們寫一個(gè)簡(jiǎn)單的對(duì)象
function Animal(name) {
this.name = name;
}
Animal.prototype.setName = function(name) {
this.name = name;
}
var animal = new Animal("wangwang");
我們可以看到,這就是一個(gè)對(duì)象Animal,該對(duì)象有個(gè)屬性name,有個(gè)方法setName。要注意,一旦修改prototype,比如增加某個(gè)方法,則該對(duì)象所有實(shí)例將同享這個(gè)方法。例如
function Animal(name) {
this.name = name;
}
var animal = new Animal("wangwang");
這時(shí)animal只有name屬性。如果我們加上一句,
Animal.prototype.setName = function(name) {
this.name = name;
}
這時(shí)animal也會(huì)有setName方法。
繼承本復(fù)制——從空的對(duì)象開始我們知道,JS的基本類型中,有一種叫做object,而它的最基本實(shí)例就是空的對(duì)象,即直接調(diào)用new Object()生成的實(shí)例,或者是用字面量{ }來聲明??盏膶?duì)象是“干凈的對(duì)象”,只有預(yù)定義的屬性和方法,而其他所有對(duì)象都是繼承自空對(duì)象,因此所有的對(duì)象都擁有這些預(yù)定義的 屬性與方法。原型其實(shí)也是一個(gè)對(duì)象實(shí)例。原型的含義是指:如果構(gòu)造器有一個(gè)原型對(duì)象A,則由該構(gòu)造器創(chuàng)建的實(shí)例都必然復(fù)制自A。由于實(shí)例復(fù)制自對(duì)象A,所以實(shí)例必然繼承了A的所有屬性、方法和其他性質(zhì)。那么,復(fù)制又是怎么實(shí)現(xiàn)的呢?方法一:構(gòu)造復(fù)制每構(gòu)造一個(gè)實(shí)例,都從原型中復(fù)制出一個(gè)實(shí)例來,新的實(shí)例與原型占用了相同的內(nèi)存空間。這雖然使得obj1、obj2與它們的原型“完全一致”,但也非常不經(jīng)濟(jì)——內(nèi)存空間的消耗會(huì)急速增加。如圖:
方法二:寫時(shí)復(fù)制這種策略來自于一致欺騙系統(tǒng)的技術(shù):寫時(shí)復(fù)制。這種欺騙的典型示例就是操作系統(tǒng)中的動(dòng)態(tài)鏈接庫(kù)(DDL),它的內(nèi)存區(qū)總是寫時(shí)復(fù)制的。如圖:
我們只要在系統(tǒng)中指明obj1和obj2等同于它們的原型,這樣在讀取的時(shí)候,只需要順著指示去讀原型即可。當(dāng)需要寫對(duì)象(例如obj2)的屬性時(shí),我們就復(fù)制一個(gè)原型的映像出來,并使以后的操作指向該映像即可。如圖:
這種方式的優(yōu)點(diǎn)是我們?cè)趧?chuàng)建實(shí)例和讀屬性的時(shí)候不需要大量?jī)?nèi)存開銷,只在第一次寫的時(shí)候會(huì)用一些代碼來分配內(nèi)存,并帶來一些代碼和內(nèi)存上的開銷。但此后就不再有這種開銷了,因?yàn)樵L問映像和訪問原型的效率是一致的。不過,對(duì)于經(jīng)常進(jìn)行寫操作的系統(tǒng)來說,這種方法并不比上一種方法經(jīng)濟(jì)。方法三:讀遍歷這種方法把復(fù)制的粒度從原型變成了成員。這種方法的特點(diǎn)是:僅當(dāng)寫某個(gè)實(shí)例的成員,將成員的信息復(fù)制到實(shí)例映像中。當(dāng)寫對(duì)象屬性時(shí),例如(obj2.value=10)時(shí),會(huì)產(chǎn)生一個(gè)名為value的屬性值,放在obj2對(duì)象的成員列表中。看圖:
可以發(fā)現(xiàn),obj2仍然是一個(gè)指向原型的引用,在操作過程中也沒有與原型相同大小的對(duì)象實(shí)例創(chuàng)建出來。這樣,寫操作并不導(dǎo)致大量的內(nèi)存分配,因此內(nèi)存的使用上就顯得經(jīng)濟(jì)了。不同的是,obj2(以及所有的對(duì)象實(shí)例)需要維護(hù)一張成員列表。這個(gè)成員列表遵循兩條規(guī)則:保證在讀取時(shí)首先被訪問到如果在對(duì)象中沒有指定屬性,則嘗試遍歷對(duì)象的整個(gè)原型鏈,直到原型為空或或找到該屬性。原型鏈后面會(huì)講。顯然,三種方法中,讀遍歷是性能最優(yōu)的。所以,JavaScript的原型繼承是讀遍歷的。constructor熟悉C++的人看完最上面的對(duì)象的代碼,肯定會(huì)疑惑。沒有class關(guān)鍵字還好理解,畢竟有function關(guān)鍵字,關(guān)鍵字不一樣而已。但是,構(gòu)造函數(shù)呢?實(shí)際上,JavaScript也是有類似的構(gòu)造函數(shù)的,只不過叫做構(gòu)造器。在使用new運(yùn)算符的時(shí)候,其實(shí)已經(jīng)調(diào)用了構(gòu)造器,并將this綁定為對(duì)象。例如,我們用以下的代碼
var animal = Animal("wangwang");
animal將是undefined。有人會(huì)說,沒有返回值當(dāng)然是undefined。那如果將Animal的對(duì)象定義改一下:
function Animal(name) {
this.name = name;
return this;
}
猜猜現(xiàn)在animal是什么?
此時(shí)的animal變成window了,不同之處在于擴(kuò)展了window,使得window有了name屬性。這是因?yàn)閠his在沒有指定的情況下,默認(rèn)指向window,也即最頂層變量。只有調(diào)用new關(guān)鍵字,才能正確調(diào)用構(gòu)造器。那么,如何避免用的人漏掉new關(guān)鍵字呢?我們可以做點(diǎn)小修改:
function Animal(name) {
if(!(this instanceof Animal)) {
return new Animal(name);
}
this.name = name;
}
這樣就萬無一失了。構(gòu)造器還有一個(gè)用處,標(biāo)明實(shí)例是屬于哪個(gè)對(duì)象的。我們可以用instanceof來判斷,但instanceof在繼承的時(shí)候?qū)ψ嫦葘?duì)象跟真正對(duì)象都會(huì)返回true,所以不太適合。constructor在new調(diào)用時(shí),默認(rèn)指向當(dāng)前對(duì)象。
console.log(Animal.prototype.constructor === Animal); // true
我們可以換種思維:prototype在函數(shù)初始時(shí)根本是無值的,實(shí)現(xiàn)上可能是下面的邏輯
// 設(shè)定__proto__是函數(shù)內(nèi)置的成員,get_prototyoe()是它的方法
var __proto__ = null;
function get_prototype() {
if(!__proto__) {
__proto__ = new Object();
__proto__.constructor = this;
}
return __proto__;
}
這樣的好處是避免了每聲明一個(gè)函數(shù)都創(chuàng)建一個(gè)對(duì)象實(shí)例,節(jié)省了開銷。constructor是可以修改的,后面會(huì)講到?;谠偷睦^承繼承是什么相信大家都差不多知道,就不秀智商下限了。
JS的繼承有好幾種,這里講兩種
1. 方法一這種方法最常用,安全性也比較好。我們先定義兩個(gè)對(duì)象
function Animal(name) {
this.name = name;
}
function Dog(age) {
this.age = age;
}
var dog = new Dog(2);
要構(gòu)造繼承很簡(jiǎn)單,將子對(duì)象的原型指向父對(duì)象的實(shí)例(注意是實(shí)例,不是對(duì)象)
Dog.prototype = new Animal("wangwang");
這時(shí),dog就將有兩個(gè)屬性,name和age。而如果對(duì)dog使用instanceof操作符
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // false
這樣就實(shí)現(xiàn)了繼承,但是有個(gè)小問題
console.log(Dog.prototype.constructor === Animal); // true
console.log(Dog.prototype.constructor === Dog); // false
可以看到構(gòu)造器指向的對(duì)象更改了,這樣就不符合我們的目的了,我們無法判斷我們new出來的實(shí)例屬于誰。因此,我們可以加一句話:
Dog.prototype.constructor = Dog;
再來看一下:
console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true
done。這種方法是屬于原型鏈的維護(hù)中的一環(huán),下文將詳細(xì)闡述。2. 方法二這種方法有它的好處,也有它的弊端,但弊大于利。先看代碼
<pre name="code" class="javascript">function Animal(name) {
this.name = name;
}
Animal.prototype.setName = function(name) {
this.name = name;
}
function Dog(age) {
this.age = age;
}
Dog.prototype = Animal.prototype;
這樣就實(shí)現(xiàn)了prototype的拷貝。
這種方法的好處就是不需要實(shí)例化對(duì)象(和方法一相比),節(jié)省了資源。弊端也是明顯,除了和上文一樣的問題,即constructor指向了父對(duì)象,還只能復(fù)制父對(duì)象用prototype聲明的屬性和方法。也即是說,上述代碼中,Animal對(duì)象的name屬性得不到復(fù)制,但能復(fù)制setName方法。最最致命的是,對(duì)子對(duì)象的prototype的任何修改,都會(huì)影響父對(duì)象的prototype,也就是兩個(gè)對(duì)象聲明出來的實(shí)例都會(huì)受到影響。所以,不推薦這種方法。
原型鏈
寫過繼承的人都知道,繼承可以多層繼承。而在JS中,這種就構(gòu)成了原型鏈。上文也多次提到了原型鏈,那么,原型鏈?zhǔn)鞘裁??一個(gè)實(shí)例,至少應(yīng)該擁有指向原型的proto屬性,這是JavaScript中的對(duì)象系統(tǒng)的基礎(chǔ)。不過這個(gè)屬性是不可見的,我們稱之為“內(nèi)部原型鏈”,以便和構(gòu)造器的prototype所組成的“構(gòu)造器原型鏈”(亦即我們通常所說的“原型鏈”)區(qū)分開。我們先按上述代碼構(gòu)造一個(gè)簡(jiǎn)單的繼承關(guān)系:
function Animal(name) {
this.name = name;
}
function Dog(age) {
this.age = age;
}
var animal = new Animal("wangwang");
Dog.prototype = animal;
var dog = new Dog(2);
提醒一下,前文說過,所有對(duì)象都是繼承空的對(duì)象的。所以,我們就構(gòu)造了一個(gè)原型鏈:
我們可以看到,子對(duì)象的prototype指向父對(duì)象的實(shí)例,構(gòu)成了構(gòu)造器原型鏈。子實(shí)例的內(nèi)部proto對(duì)象也是指向父對(duì)象的實(shí)例,構(gòu)成了內(nèi)部原型鏈。當(dāng)我們需要尋找某個(gè)屬性的時(shí)候,代碼類似于
function getAttrFromObj(attr, obj) {
if(typeof(obj) === "object") {
var proto = obj;
while(proto) {
if(proto.hasOwnProperty(attr)) {
return proto[attr];
}
proto = proto.__proto__;
}
}
return undefined;
}
在這個(gè)例子中,我們?nèi)绻赿og中查找name屬性,它將在dog中的成員列表中尋找,當(dāng)然,會(huì)找不到,因?yàn)楝F(xiàn)在dog的成員列表只有age這一項(xiàng)。接著它會(huì)順著原型鏈,即.proto指向的實(shí)例繼續(xù)尋找,即animal中,找到了name屬性,并將之返回。假如尋找的是一個(gè)不存在的屬性,在animal中尋找不到時(shí),它會(huì)繼續(xù)順著.proto尋找,找到了空的對(duì)象,找不到之后繼續(xù)順著.proto尋找,而空的對(duì)象的.proto指向null,尋找退出。
原型鏈的維護(hù)我們?cè)趧偛胖v原型繼承的時(shí)候提出了一個(gè)問題,使用方法一構(gòu)造繼承時(shí),子對(duì)象實(shí)例的constructor指向的是父對(duì)象。這樣的好處是我們可以通過constructor屬性來訪問原型鏈,壞處也是顯而易見的。一個(gè)對(duì)象,它產(chǎn)生的實(shí)例應(yīng)該指向它本身,也即是
(new obj()).prototype.constructor === obj;
然后,當(dāng)我們重寫了原型屬性之后,子對(duì)象產(chǎn)生的實(shí)例的constructor不是指向本身!這樣就和構(gòu)造器的初衷背道而馳了。我們?cè)谏厦嫣岬搅艘粋€(gè)解決方案:
Dog.prototype = new Animal("wangwang");
Dog.prototype.constructor = Dog;
看起來沒有什么問題了。但實(shí)際上,這又帶來了一個(gè)新的問題,因?yàn)槲覀儠?huì)發(fā)現(xiàn),我們沒法回溯原型鏈了,因?yàn)槲覀儧]法尋找到父對(duì)象,而內(nèi)部原型鏈的.proto屬性是無法訪問的。于是,SpiderMonkey提供了一個(gè)改良方案:在任何創(chuàng)建的對(duì)象上添加了一個(gè)名為__proto__的屬性,該屬性總是指向構(gòu)造器所用的原型。這樣,對(duì)任何constructor的修改,都不會(huì)影響__proto__的值,就方便維護(hù)constructor了。
但是,這樣又兩個(gè)問題:
__proto__是可以重寫的,這意味著使用它時(shí)仍然有風(fēng)險(xiǎn)
__proto__是spiderMonkey的特殊處理,在別的引擎(例如JScript)中是無法使用的。
我們還有一種辦法,那就是保持原型的構(gòu)造器屬性,而在子類構(gòu)造器函數(shù)內(nèi)初始化實(shí)例的構(gòu)造器屬性。
代碼如下:改寫子對(duì)象
function Dog(age) {
this.constructor = arguments.callee;
this.age = age;
}
Dog.prototype = new Animal("wangwang");
這樣,所有子對(duì)象的實(shí)例的constructor都正確的指向該對(duì)象,而原型的constructor則指向父對(duì)象。雖然這種方法的效率比較低,因?yàn)槊看螛?gòu)造實(shí)例都要重寫constructor屬性,但毫無疑問這種方法能有效解決之前的矛盾。ES5考慮到了這種情況,徹底的解決了這個(gè)問題:可以在任意時(shí)候使用Object.getPrototypeOf() 來獲得一個(gè)對(duì)象的真實(shí)原型,而無須訪問構(gòu)造器或維護(hù)外部的原型鏈。因此,像上一節(jié)所說的尋找對(duì)象屬性,我們可以如下改寫:
function getAttrFromObj(attr, obj) {
if(typeof(obj) === "object") {
do {
var proto = Object.getPrototypeOf(dog);
if(proto[attr]) {
return proto[attr];
}
}
while(proto);
}
return undefined;
}
當(dāng)然,這種方法只能在支持ES5的瀏覽器中使用。為了向后兼容,我們還是需要考慮上一種方法的。更合適的方法是將這兩種方法整合封裝起來,這個(gè)相信讀者們都非常擅長(zhǎng),這里就不獻(xiàn)丑了。
相關(guān)文章
通過JS和PHP兩種方法判斷用戶請(qǐng)求時(shí)使用的瀏覽器類型
在做微站點(diǎn)項(xiàng)目開發(fā)的時(shí)候,我們需要判斷當(dāng)前瀏覽器類型是什么。接下來小編給大家分享通過JS和PHP兩種方法判斷用戶請(qǐng)求時(shí)使用的瀏覽器類型,非常不錯(cuò),感興趣的朋友一起學(xué)習(xí)吧2016-09-09JS查找字符串中出現(xiàn)最多的字符及個(gè)數(shù)統(tǒng)計(jì)
最近在項(xiàng)目中遇到這樣的需求:求字符串'nininihaoa'中出現(xiàn)次數(shù)最多字符。怎么實(shí)現(xiàn)呢?下面小編給大家分享具體實(shí)現(xiàn)代碼,需要的朋友參考下吧2017-02-02Bootstrap禁用響應(yīng)式布局的實(shí)現(xiàn)方法
這篇文章主要介紹了Bootstrap禁用響應(yīng)式布局的實(shí)現(xiàn)方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03微信小程序?qū)W習(xí)筆記之表單提交與PHP后臺(tái)數(shù)據(jù)交互處理圖文詳解
這篇文章主要介紹了微信小程序?qū)W習(xí)筆記之表單提交與PHP后臺(tái)數(shù)據(jù)交互處理,結(jié)合實(shí)例形式詳細(xì)分析了微信小程序前臺(tái)數(shù)據(jù)form表單提交及后臺(tái)使用php進(jìn)行處理相關(guān)操作技巧,并配以圖文形式詳細(xì)說明,需要的朋友可以參考下2019-03-03基于BootStrap multiselect.js實(shí)現(xiàn)的下拉框聯(lián)動(dòng)效果
當(dāng)option特別多時(shí),一般的下拉框選擇起來就有點(diǎn)力不從心了,所以使用multiselect是個(gè)很好的選擇。在網(wǎng)上找了半天找到了解決方案,具體實(shí)現(xiàn)代碼大家參考下本文吧2017-07-07javascript下數(shù)值型比較難點(diǎn)說明
下面兩個(gè)小問題是樓豬在實(shí)際項(xiàng)目開發(fā)中遇到的,貼上來和大家討論下。2010-06-06淺談Sublime Text 3運(yùn)行JavaScript控制臺(tái)
下面小編就為大家?guī)硪黄獪\談Sublime Text 3運(yùn)行JavaScript控制臺(tái)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-06-06