第五章 繼承

學(xué)習(xí)如何創(chuàng)建對象時理解面向?qū)ο蟮牡谝徊健5诙繒r理解繼承。在傳統(tǒng)面向?qū)ο蟮恼Z言中,類從其他類繼承屬性。然而在javascript中,繼承可以發(fā)生在沒有類的繼承關(guān)係的對象之間。如果你看過之前的文章,那麼這種繼承機制你已經(jīng)熟悉了。就是原型對象。

5.1 原型對象鏈和Object.prototype

javascript內(nèi)建的繼承方法被成爲(wèi)原型對象鏈,又可稱爲(wèi)原型對象繼承。如你在第四章所學(xué),原型對象的屬性可經(jīng)由對象實例訪問,這就是繼承的一種形式,對象實例繼承了原型對象的屬性。因爲(wèi)原型對象也是一種對象,它也有自己的原型對象並繼承其屬性,這就是原型對象鏈:對象繼承其原型對象,而原型對象繼承它的原型對象,以此類推。

所有的對象,包括那些你自己定義的對象都自動繼承自O(shè)bject,除非你另有指定。更確切的說,所有對象都繼承自O(shè)bject.prototype。任何以對象字面形式定義的對象,其[[Prototype]]的值都被設(shè)爲(wèi)Object.prototype。這意味著它繼承Object.prototype的屬性,

var book={

title:"The Principles of Object-Oriented JavaScript"

};

var prototype=Object.getPrototypeOf(book);

console.log(prototype === Object.prototype); //true

book 的對象原型是Object.prototype。這裏不需要多餘的代碼來指定,因爲(wèi)這是創(chuàng)新建對象的默認(rèn)行爲(wèi)。這個關(guān)係意味著book會自動接受來自O(shè)bject.prototype的方法。

5.1.1 繼承自O(shè)bject.prototype的方法

前幾章裏用到的多個方法其實都定義在Object.prototype上的,因此可以被其他對象繼承,這些方法如下。

hasOwnProperty() ? ? ? ? ? ? ? ? ? ?檢查是否存在一個給定名字的自有屬性

propertyIsEnumerable() ? ? ? ? ?檢查一個自有屬性是否可枚舉

isPrototypeOf() ? ? ? ? ? ? ? ? ? ? ?檢查一個對象是否是另一個對象的原型對象

valueOf ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?返回一個對象的值表達(dá)

toString() ? ? ? ? ? ? ? ? ? ? ? ? ? 返回一個對象的字符串表達(dá)

這5種方法經(jīng)由繼承出現(xiàn)在所有對象中。當(dāng)需要讓對象在javascript中以一致的方式工作時,最後兩個尤其重要,有時甚至?xí)肫鹨约憾x它們。

1. valueOf()

每當(dāng)一個操作符被用於一個對象時就會調(diào)用 valueOf()方法。valueOf()默認(rèn)返回對象實例本身。原始封裝類型重寫鏈valueOf

()以使得它對String返回一個字符串,對Boolean返回一個布爾,對Number返回一個數(shù)字。類似的,Date對象的valueOf()方法返回一個epoch實際,單位時毫秒(正如Date.prototype.getTime()所爲(wèi))。

這允許你寫出下列代碼來對Date做比較。

var now = new Date();

var earlier=new Date(2010,1,1);

console.log(now >earlier); //true

本例中,now時一個代表當(dāng)前實際的Date,而earlier時一個過去的實際,當(dāng)使用大於操作符時,在實際比較錢,在兩個對象上都調(diào)用了valueOf()方法。你甚至可以對兩個Date相減來獲得它們在epoch事件上的差值。

如果你的對象也要這樣使用操作符,你可以定義自己的valueOf()方法。定義的時候你並沒有改變操作符的行爲(wèi),僅僅定義了操作符默認(rèn)行爲(wèi)使用的值。

2. toString()

一旦valueOf()返回的時一個引用而不是原始值的時候,就會回退調(diào)用toString()方法。另外,當(dāng)javascript期望一個字符串時,也可以對原始值隱式調(diào)用toString()。例如,當(dāng)加號操作符的一個邊時一個字符串時,另一邊會自動轉(zhuǎn)換成字符串。如果另一邊時一個原始值,會自動被轉(zhuǎn)換成一個字符串表達(dá),如果另一邊時一個引用值,則會調(diào)用valueOf()。如果valueOf()返回一個引用值,則調(diào)用toString()。如下面代碼。

var book={

title:"The Principles of Object-Oriented JavaScript"

};

var message ="Book ="+book;

console.log(message);

這段代碼以"Book="和book來構(gòu)造字符串。因爲(wèi)book是一個對象,此時調(diào)用它的toString()方法。該方法繼承自O(shè)bject。prototype,大部分javascript引擎返回默認(rèn)值"[object Object]"。 如過你對這個值滿意,雞不需要改變對象的toString()方法。但定義自己的toString()方法有時可以爲(wèi)此類字符串轉(zhuǎn)換提供包含更多信息的值,假設(shè)你想要之前的腳本記錄的名字。請看下例。

var book={

title:"The Principle ?of Object-Oriented JavaScript",

toString:function(){

return "[Book"+this.title +"]"

}};

var message ="Book = "+book;

console.log(message);

這段代碼爲(wèi)book自定義的toString()方法與繼承來的版本相比,返回更有用的值。大多數(shù)時候,你不需要自定義toString()方法,但必要時你該知道怎麼做。?

5.1.2 修改Object.prototype

所有的對象某默認(rèn)繼承自O(shè)bject.prototype,所以改變Object.prototype會影響所有的對象,這是非常危險的,第四章告誡過你不要修改內(nèi)建對象的原型對象,到了Obejct.prototype,這個告誡要加倍。查看下面的代碼會發(fā)生什麼。

Object.prototype.add=function(value){

return this+value;

}

var book={

title:"The Principles of Object-Oriented JavaScript"

};

console.log(book.add(5));

console.log("title".add("end"));

console.log(document.add(true));

console.log(window.add(t));

添加Object.prototype.add()會導(dǎo)致所有的對象都有了一個add()方法,不管這樣合理不合理。不僅僅給開發(fā)者,同時也給Javascript委員會帶來了問題:它不得不把新方法添加到各種不同的地方,因爲(wèi)給Object.prototype添加方法可能會帶來不可預(yù)知的問題。

這個問題的另一個方面在於給Object.prototype添加可枚舉屬性。在之前的例子裏,Object.prototype.add()是一個可枚舉屬性,這意味著它會出現(xiàn)在for-in循環(huán)中,如下。

var empty={}

for(var property in empty){

console.log(property)};

這裏,一個空對象依然會暑促“add”作爲(wèi)其屬性,就是因爲(wèi)它存在與其原型對里且爲(wèi)可 枚舉屬性。考慮到j(luò)avascript中使用for-in頻繁的程度,爲(wèi)Object.prototype添加可枚舉屬性會影響大量代碼,因爲(wèi)這個原因,Douglas Crockford推薦在for-in循環(huán)中始終使用hasOwnProperty(),如下;

var empty={};

for(var property in empty){

if(empty hasOwnProperty(property)){

console.log(property);

};

}

不過這個方法雖然可以有效過濾那些不想要的原型對象的屬性,但也同時限制了for-in屬性,使其只能用於自有屬性,這也許不是你想要的,對你來說,最靈活的做法還是不要修改Object.prototype。

5.2 對象繼承

對象繼承是最簡單的繼承類型。你唯一需要要做的就是指定哪個對象時新對象的[[Prototype]]。對象字面形式都會隱式指定Object.prototype爲(wèi)其[[Prototype]],你可以可以使用Object.create()方法顯式指定。

Object.create()接受兩個參數(shù),第一個參數(shù)時需要被設(shè)置爲(wèi)新對象的[[Prototype]]的對象。第二個可選參數(shù)時一個屬性描述對象,其格式如你在Object.fineProperties()中使用的一樣。

var book ={

title:"The Principles of Object-Oriented JavaScript"

};

//is the same as

var book=Object.create(Object.prototype,{

title:{

configurable:true,

enumerable:true,

value:"The Principles of Object-Oriented JavaScript",

writable:true

}

});

兩種聲明具有相同的效果。第一種聲明使用對象字面形式來定義一個具有單一屬性title的對象。該對象自動繼承自O(shè)bject.prototype,且其屬性別默認(rèn)設(shè)置爲(wèi)可配置,可枚舉和可寫。第二種聲明使用Obejct.create()顯示做了同樣的操作。兩個book對象的行爲(wèi)完全一致。但你可能永遠(yuǎn)不會這樣寫出直接繼承自O(shè)bject.prototype的代碼,畢竟那是默認(rèn)行爲(wèi)。繼承自其他對象則更有趣多了。如下。

var person1={

name:"Nicholas",

sayName:function(){

console.log(this.name);

}

};

var person2=Object.create(person1,{

name:{

configurable:true,

enumerable:true,

value:"Greg",

writable:true

}

});

person1.sayName();

person2.sayName();

console.log(person1.hasOwnProperty("sayName"));

console.log(person1.isPrototypeOf(person2));

console.log(person2.hasOwnProperty("sayName"));



這段代碼創(chuàng)建來一個對象person1,具有一個name屬性和一個sayName()方法。對象person2繼承自person1,也就繼承來name和sayName()。然而person2在通過Object.create()創(chuàng)建時還定義了一個自有屬性name()。該自有屬性隱藏並太低了原型對象的同名屬性。所以,person1.sayName()輸出“Nicholas”,而person2.sayName()輸出“Greg”。

請記住,sayName()依然只存在於person1並被person2繼承。

本例person2的繼承鏈長於person1.對像person2繼承自person1而person1繼承自O(shè)bject.prototype。

當(dāng)訪問一個對象的屬性時,javascript引擎會執(zhí)行一個搜索過程。如果在對象實例上發(fā)現(xiàn)該屬性(就是說是個自有屬性),該屬性值就會被使用。如果對象實例上沒有發(fā)現(xiàn)該屬性,則搜索[[Prototype]]。如果仍然沒有發(fā)現(xiàn),則繼續(xù)搜索該對象的[[Prototype]],知道繼承鏈末端,末端通常時一個Object.prototype,其[[Prototype]]被置爲(wèi)null。

也可以通過Object.create()創(chuàng)建[[Prototype]爲(wèi)null的對象,

var nakedObject=Object.create(null);

console.log("toString" in nakedObject);

console.log("valueOf" in nakedObject);

本例中的nakedObject是 一個沒有原型對象鏈的對象。這意味著toString()和valueOf()等內(nèi)建方法都不存在於該對象上。實際上,該對象完全是一個沒有任何預(yù)定義屬性的白板,這使得它稱爲(wèi)一個完美的哈希容器,因爲(wèi)兒不會發(fā)生跟繼承來的屬性名字衝突。除此之外這種對象也沒有別的用處啦,你不能把它當(dāng)成一個其他繼承自O(shè)bject.prototype的對象一樣使用。例如,無論何時當(dāng)你對nakedObject使用操作符時,你都會得到一個“Cannot convert object to primitive value”的錯誤。這只是一個有趣的javascript語言詭計,是你可以創(chuàng)建出一個沒有原型對象的對象。

5.3 構(gòu)造函數(shù)繼承

javascript中的對象繼承也是構(gòu)造函數(shù)繼承的基礎(chǔ)。還記得第四章提到,幾乎所有的函數(shù)多有prototype屬性,它可以被修改或替換。該prototype屬性被自動設(shè)置爲(wèi)一個新的繼承自O(shè)bejct.prototype的泛用對象,該對象有一個自有屬性constructor。實際上,javascript引擎爲(wèi)你做了下面的事情。

function YourConstructor(){

//initialization

}

//JavaScript engine does this for you behind the scenes

YourConstructor.prototype=Object.create(Object.prototype){

constructor:{

configurable:true,

enumerable:true,

value:YourConstructor,

writable:true

}

});

你不需要做額外的工作,這段代碼幫你把構(gòu)造函數(shù)的prototype屬性設(shè)置爲(wèi)一個繼承自O(shè)bject.prototype的對象。這意味著YourConstructor創(chuàng)建出來的任何對象都繼承自O(shè)bject.prototype。YourConstructor是Object的子類。而Object時YourConstructor的父類。

由於prototype屬性可寫,你可以通過改寫它來改變原型對象鏈。考慮下面的例子。

function Rectangle(length,width){

this.length =length;

this.width =width;

}

Rectangle.prototype.getArea=function(){

return this.length*this.width;

};

Rectangle.prototype.toString =function(){

return "[Rectangle"+this.length+"x"+this.width+"]";

};

function Square(size){

this.length=size;

this.width=size;

}

Square.prototype=new Rectangle();

Square.prototype.constructor=Square;

Square.prototype.toString =function(){

return "[Square"+this.length+"X"+this.width+"]";

};

var rect=new Rectangle(5,10);

var square=new Square(6);

console.log(rect.getArea());

console.log(square.getArea());

console.log(rect.toString());

console.log(square.toString());

console.log(rect instanceof Square);

console.log(rect instanceof Object);

console.log(square instanceof Square);

console.log(square instanceof Rectangle);

console.log(square instanceof Object);

這段代碼里有兩個構(gòu)造函數(shù):Rectangle和Square。Square構(gòu)造函數(shù)的prototype屬性被改寫爲(wèi)Rectangle的一個對象實例。此時不需要給Rectangle的調(diào)用提供參數(shù),因爲(wèi)它們不需要被使用,而且如果提供了,那麼所有的Square的對象都會共享同樣的唯獨。用這種方式改版原型對象鏈時,你需要確保構(gòu)造函數(shù)不會在參數(shù)確實時拋出錯誤(很多構(gòu)造函數(shù)包含的初始化邏輯會需要參數(shù))且構(gòu)造函數(shù)不會改變?nèi)魏稳譅顟B(tài),比如追蹤有多少對象實例被創(chuàng)建等。Square.prototype被改寫後,其constructor屬性會被重置爲(wèi)Square。

然後,rect作爲(wèi)Rectangle的對象實例被創(chuàng)建,而square則被作爲(wèi)Square的實例創(chuàng)建,兩個對象都有g(shù)etArea()方法,因爲(wèi)那繼承自Square.prototype。instanceof操作符認(rèn)爲(wèi)變量square同時是Square,Rectangle和Object的對象實例,因爲(wèi)instanceof使用原型對象鏈檢查對象類型。

Square.prototype並不真的需要被改寫爲(wèi)一個Rectangle敵對性,比較Rectangle構(gòu)造函數(shù)並沒有真的爲(wèi)Square做什麼必須要的事情。事實上,唯一相關(guān)的部分是Square.prototype需要指向Rectangle.prototype,使得繼承得以實現(xiàn)。這意味著你可以用Object.create()簡化例子

function Square(size){

this.length=size;

this.width=size;

};

Square.prototype=Object.create(Rectangle.prototype,{

constructor:{

configurable:true,

enumerable:true,

value:Square,

writable:true

}

});

Square.prototype.toString=function(){

return " [Square"+this.length+"X"+this.width+"]";

};

在這個版本的代碼中,Square.prototype被改寫成爲(wèi)一個新的繼承自Rectangle.prototype的對象,而Rectangle構(gòu)造函數(shù)沒有被調(diào)用。這意味著,你不再需要擔(dān)心不參加調(diào)用構(gòu)造函數(shù)會導(dǎo)致的錯誤。除此之外,這段代碼和前面的代碼行爲(wèi)完全一致。原型對象鏈完好無缺,所有的Square對象實例都繼承自Rectangle.prototype且其constructor屬性也都在同樣的地方被重置。

注意:在對原型對象添加屬性前要確保你已經(jīng)改寫鏈原型對象,否則在改寫前會丟失之前的方法。

5.4 構(gòu)造函數(shù)的竊取

由於javascript中的繼承是通過原型對象鏈來實現(xiàn)的,因此不需要調(diào)用對象的父類的構(gòu)造函數(shù),如果你確實需要在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù),那你就需要利用javascript函數(shù)工作的特性。

在第二章中學(xué)過call()和apply()方法允許你在調(diào)用函數(shù)時提供不同的this值。那正好時構(gòu)造函數(shù)竊取的關(guān)鍵。只需要在子類的構(gòu)造函數(shù)中用call()或者apply()調(diào)用父類的構(gòu)造函數(shù),並將新的對象傳進(jìn)去即可。實際上,就是用自己的對象竊取父類的構(gòu)造函數(shù)。如下例。

function Rectangle(length,width){

this.length =length;

this.width=width;

};

Rectangle.prototype.getArea=function(){

return this.length*this.width;

};

Rectangle.prototype.toString=function(){

return "[Rectangle"+this.length+"X"+this.width+"]";

};

//inherits from Rectangle

function Square(size){

Rectangle.call(this,size,size);

//optional:add new properties of override existing ones here

}

Square.prototype=Object.create(Rectangle.prototype,{

constructor:{

configurable:true,

enumerable:true,

value:Square,

writable:true

}

});

Square.prototype.toString()=function(){

return "[Rectangle"+this.length+"X"+this.width+"]";

}

var square=new Square(6);

console.log(square,length);

console.log(square.width);

console.log(square.getArea());

Square構(gòu)造函數(shù)調(diào)用了Rectangle構(gòu)造函數(shù),並傳入了this和size兩次(一次作爲(wèi)length,另一次作爲(wèi)width)。這麼做會在新對象上創(chuàng)建length和width屬性並讓它們等於size,這是一種避免在構(gòu)造函數(shù)里重新定義你希望繼承的屬性的手段。你可以在調(diào)用完父類的構(gòu)造函數(shù)後繼續(xù)添加新的屬性或覆蓋已有的屬性。

這個分兩步走的過程在你需要完成自定義類型之間的繼承時比較喲用。你經(jīng)常需要修改一個構(gòu)造函數(shù)。你也經(jīng)常需要在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)。一般來說,需要修改prototype來繼承方法並用構(gòu)造函數(shù)竊取來設(shè)置屬性。由於這種做法模仿了那些基於類的語言的類繼承,通常被稱爲(wèi)僞類繼承。

5.5 訪問父類方法

在前面的例子中,Square類型有自己的toString()方法隱藏類其原型對象的toString()方法。子類提供新功能覆蓋父類的方法十分常見,但如果你還想訪問父類的方法該怎麼辦呢?在其他語言中,可以用super.toString(),但在javascript中沒有類似的方式。代替的方法是通過call()或apply()調(diào)用父類的原型對象的方法時傳入一個子類的對象。

function Rectangle(length,width)(

this.length =length;

this.width=width;

)

Rectangle.prototype.getArea=function(){

return this.length*this.width;

};

Rectangle.prototype.toString=function(){

return "[Rectangle"+this.length+"X"+this.width+"]";

}

function Square(size){

Rectangle.call(this,size,size);

};

Square.prototype=Object.create(Rectangle.prototype,{

constructor:{

configurable:true,

enumerable:true,

value:Square,

writable:true

}

});

square.prototype.toString=function(){

var text=Rectangle.prototype.toString.call(this);

return text.replace("Rectangle","Square");

}

在這個版本的代碼中,Square.prototype.toString()通過call()調(diào)用Rectangle.prototype.toString()。該方法只需要在返回文本結(jié)果前用"Square"替換"Rectangle"。這種做法看上去可能有一點冗長,但這是唯一的訪問父類方法的手段。

5.6 總結(jié)

javascript通過原型對象鏈支持繼承。當(dāng)將一個對象的[[Prototype]]設(shè)置爲(wèi)另一個對象時,就在這兩個對象之間創(chuàng)建了一條原型鏈。所有的泛用對象都自動繼承自O(shè)bject.prototype。如果你想要創(chuàng)建一個繼承自其他對象的對象,你可以用Object.create()指定[[Prototype]]爲(wèi)一個新的對象。

可以在構(gòu)造函數(shù)中創(chuàng)建原型對象鏈來完成自定義類型之間的繼承。通過將構(gòu)造函數(shù)的prototype屬性設(shè)置爲(wèi)某一個對象,就建立了自定義類型對象和該對象的繼承關(guān)係。構(gòu)造函數(shù)的多有對象實例共享同一個原型對象,所以它們都繼承自該對象。這個技術(shù)在繼承其他對象的方法時工作得時分好,但你不能用原型對象繼承自有屬性。

爲(wèi)了正確繼承自由屬性,可以使用構(gòu)造函數(shù)竊取。只需要call()或apply()調(diào)用父類的構(gòu)造函數(shù),就可以在子類里完成各種初始化。結(jié)合構(gòu)造函數(shù)竊取和原型對象鏈時javascript中最常見的繼承手段。由於和基於類的繼承相似,這個組合經(jīng)常被稱爲(wèi)僞類繼承。可以通過直接訪問父類原型對象的方式訪問父類的方法。當(dāng)你這麼做時,你必須以call()或apply()執(zhí)行父類方法並傳入一個子類的對象。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • javascript有很多創(chuàng)建對象的模式,完成工作的方式也不只一種。你可以隨時定義自己的類型或自己的泛用對象。可以...
    WanLum閱讀 269評論 0 0
  • 盡管javascript里有大量內(nèi)建引用對象,很可能你還說會頻繁創(chuàng)建自己的對象。當(dāng)你在這么做的時候,記得javas...
    WanLum閱讀 540評論 1 3
  • 也許你還沒有理解構(gòu)造函數(shù)和原型對象的時候已經(jīng)在javascript的路上走了很久,但直到你很好的掌握它們之前你不會...
    WanLum閱讀 417評論 0 1
  • 作者:王納米 首發(fā)於《代碼指南》(daimazhinan.com) 《代碼指南》編輯部保留所有權(quán)利 1 What ...
    王納米其實是病弱美少年閱讀 861評論 2 7
  • 大多數(shù)的開發(fā)者在使用Java或C#等基于類的語言的過程中學(xué)會了面向?qū)ο缶幊獭S捎贘avaScript沒有對類的正式...
    WanLum閱讀 597評論 0 4