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