JavaScript中的原型和繼承詳解(圖文)
請(qǐng)?jiān)诖藭簳r(shí)忘記之前學(xué)到的面向?qū)ο蟮囊磺兄R(shí)。這里只需要考慮賽車(chē)的情況。是的,就是賽車(chē)。
最近我正在觀看 24 Hours of Le Mans ,這是法國(guó)流行的一項(xiàng)賽事。最快的車(chē)被稱為 Le Mans 原型車(chē)。這些車(chē)雖然是由“奧迪”或“標(biāo)致”這些廠商制造的,可它們并不是你在街上或速公路上所見(jiàn)到的那類(lèi)汽車(chē)。它們是專為參加高速耐力賽事而制造出來(lái)的。
廠家投入巨額資金,用于研發(fā)、設(shè)計(jì)、制造這些原型車(chē),而工程師們總是努力嘗試將這項(xiàng)工程做到極致。他們?cè)诤辖?、生物燃料、制?dòng)技術(shù)、輪胎的化合物成分和安全特性上進(jìn)行了各種實(shí)驗(yàn)。隨著時(shí)間的推移,這些實(shí)驗(yàn)中的某些技術(shù)經(jīng)過(guò)反復(fù)改進(jìn),隨之進(jìn)入到車(chē)輛的主流產(chǎn)品線中。你所駕駛車(chē)輛的某些技術(shù),有可能是在賽車(chē)原型上第一次亮相的。
你也可以說(shuō),這些主流車(chē)輛繼承了來(lái)自賽車(chē)的技術(shù)原型。
到現(xiàn)在,我們就有討論 JavaScript 中的原型和繼承問(wèn)題的基礎(chǔ)了。它雖然并不像你在 C++、Java 或 C# 中了解的經(jīng)典繼承模式一樣,但這種方式同樣強(qiáng)大,并且有可能會(huì)更加靈活。
有關(guān)對(duì)象和類(lèi)
JavaScript 中全是對(duì)象,這指的是傳統(tǒng)意義上的對(duì)象,也就是“一個(gè)包含了狀態(tài)和行為的單一實(shí)體”。例如,JavaScript 中的數(shù)組是含有數(shù)個(gè)值,并且包含 push、reverse 和 pop 方法的對(duì)象。
var myArray = [1, 2]; myArray.push(3); myArray.reverse(); myArray.pop(); var length = myArray.length;
現(xiàn)在問(wèn)題是,push 這樣的方法是從何而來(lái)的呢?我們前面提到的那些靜態(tài)語(yǔ)言使用“類(lèi)語(yǔ)法”來(lái)定義對(duì)象的結(jié)構(gòu),但是 JavaScript 是一個(gè)沒(méi)有“類(lèi)語(yǔ)法”的語(yǔ)言,無(wú)法用 Array“類(lèi)”的語(yǔ)法來(lái)定義每個(gè)數(shù)組對(duì)象。而因?yàn)?JavaScript 是動(dòng)態(tài)語(yǔ)言,我們可以在實(shí)際需要的情況下,將方法任意放置到對(duì)象上。例如下面的代碼,就在二維空間中,定義了用來(lái)表示一個(gè)點(diǎn)的點(diǎn)對(duì)象,同時(shí)還定義了一個(gè) add 方法。
var point = { x : 10, y : 5, add: function(otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } };
但是上面的做法可擴(kuò)展性并不好。我們需要確保每一個(gè)點(diǎn)對(duì)象都含有一個(gè) add 方法,同時(shí)也希望所有點(diǎn)對(duì)象都共享同一個(gè) add 方法的實(shí)現(xiàn),而不是這個(gè)方法手工添加每一個(gè)點(diǎn)對(duì)象上。這就是原型發(fā)揮它作用的地方。
有關(guān)原型
在 JavaScript 中,每個(gè)對(duì)象都保持著一塊隱藏的狀態(tài) —— 一個(gè)對(duì)另一個(gè)對(duì)象的引用,也被稱作原型。我們之前創(chuàng)建的數(shù)組引用了一個(gè)原型對(duì)象,我們自行創(chuàng)建的點(diǎn)對(duì)象也是如此。上面說(shuō)原型引用是隱藏的,但也有 ECMAScript(JavaScript 的正式名稱)的實(shí)現(xiàn)可以通過(guò)一個(gè)對(duì)象的__proto__屬性(例如谷歌瀏覽器)訪問(wèn)到這個(gè)原型引用。從概念上講,我們可以將對(duì)象當(dāng)作類(lèi)似于 圖1 所表示的對(duì)象 —— 原型的關(guān)系。
圖 1
展望未來(lái),開(kāi)發(fā)者將能夠使用 Object.getPrototypeOf 函數(shù),代替__proto__屬性,取得對(duì)象原型的引用。在本文寫(xiě)出的時(shí)候,已經(jīng)可以在 Google Chrome,F(xiàn)Irefox 和 IE9 瀏覽器中使用 Object.getPrototypeOf 函數(shù)。更多瀏覽器在未來(lái)會(huì)實(shí)現(xiàn)此功能,因?yàn)樗呀?jīng)是 ECMAScript 標(biāo)準(zhǔn)的一部分了。我們可以使用下面的代碼,來(lái)證明我們建立的 myArray 和點(diǎn)對(duì)象引用的是兩個(gè)不同的原型對(duì)象。
- Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);
對(duì)于本文的其余部分,我將交叉使用 __proto__和Object.getPrototypeOf 函數(shù),主要是因?yàn)?__proto__ 在圖和句子中更容易識(shí)別。需要記住的是它(__proto__)不是標(biāo)準(zhǔn),而 Object.getPrototypeOf 函數(shù)才是查看對(duì)象原型的推薦方法。
是什么讓原型如此特別?
我們還沒(méi)有回答這個(gè)問(wèn)題:數(shù)組中 push 這樣的方法是從何而來(lái)的呢?答案是:它來(lái)源于 myArray 原型對(duì)象。圖 2 是 Chrome 瀏覽器中腳本調(diào)試器的屏幕截圖。我們已經(jīng)調(diào)用 Object.getPrototypeOf 方法查看 myArray 的原型對(duì)象。
圖 2
注意 myArray 的原型對(duì)象中有許多方法,包括那些在代碼示例中調(diào)用的 push、pop 和 reverse 方法。因此,原型對(duì)象中的確包括 push 方法,但是 myArray 方法如何引用到呢?
myArray.push(3);
了解其工作原理的第一步,是要認(rèn)識(shí)到原型并不是特別的。原型只是普通的對(duì)象。可以給原型添加方法,屬性,并把他們當(dāng)作其他 JavaScript 對(duì)象一樣看待。然而,套用喬治·奧威爾的小說(shuō)《動(dòng)物農(nóng)場(chǎng)》中“豬”的說(shuō)法 —— 所有的對(duì)象應(yīng)當(dāng)是平等的,但有些對(duì)象(遵守規(guī)則的)比其他人更加平等。
JavaScript 中的原型對(duì)象的確是特殊的,因?yàn)樗麄冏駨囊韵乱?guī)則。當(dāng)我們告訴 JavaScript 我們要調(diào)用一個(gè)對(duì)象的 push 方法,或讀取對(duì)象的 x 屬性時(shí),運(yùn)行時(shí)會(huì)首先查找對(duì)象本身。如果運(yùn)行時(shí)找不到想要的東西,它就會(huì)循著 __proto__ 引用和對(duì)象原型尋找該成員。當(dāng)我們 調(diào)用 myArray 的 push 方法時(shí),JavaScript 并沒(méi)有在 myArray 對(duì)象上發(fā)現(xiàn) push 方法,而是在 myArray 的原型對(duì)象上找到了,于是 JavaScript 調(diào)用此方法(見(jiàn)圖 3)。
圖 3
上面所描述的行為是指一個(gè)對(duì)象本身繼承了原型上的任何方法或?qū)傩?。JavaScript 中其實(shí)不需要使用類(lèi)語(yǔ)法也能實(shí)現(xiàn)繼承。就像從賽車(chē)原型上繼承了相應(yīng)的技術(shù)的車(chē),一個(gè) JavaScript 對(duì)象也可以從原型對(duì)象上繼承功能特性。
圖 3 還展示了每個(gè)數(shù)組對(duì)象同時(shí)也可以維護(hù)自身的狀態(tài)和成員。在請(qǐng)求得到 myArray 的 length 屬性的情況下,JavaScript 會(huì)取得 myArray 中 length 屬性的值,而不會(huì)去讀取原型中的對(duì)應(yīng)值。我們可以通過(guò)向?qū)ο笊咸砑?push 這樣的方法來(lái)“重寫(xiě)”push 方法。這樣就會(huì)有效地隱藏原型中的 push 方法實(shí)現(xiàn)。
共享原型
JavaScript 中原型的真正神奇之處是多個(gè)對(duì)象如何維持對(duì)同一個(gè)原型對(duì)象的引用。例如,如果我們創(chuàng)建了這樣的兩個(gè)數(shù)組:
var myArray = [1, 2]; var yourArray = [4, 5, 6];
那么這兩個(gè)數(shù)組將共享同一個(gè)原型對(duì)象,而下面的代碼計(jì)算結(jié)果為 true:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我們引用兩個(gè)數(shù)組對(duì)象上的 push 方法,JavaScript 會(huì)去尋找原型上共享的 push 方法。
圖 4
JavaScript 中的原型對(duì)象提供繼承功能,同時(shí)也就實(shí)現(xiàn)了該方法實(shí)現(xiàn)的共享。原型也是鏈?zhǔn)降摹Q句話說(shuō),因?yàn)樵蛯?duì)象只是一個(gè)對(duì)象,所以一個(gè)原型對(duì)象可以維持到另一個(gè)原型對(duì)象的引用。如果你重新審視圖 2 便可以看到,原型的 __proto__ 屬性是一個(gè)指向另一個(gè)原型的非空值。當(dāng) JavaScript 查找像 push 方法這樣的成員時(shí),它會(huì)循著原型引用鏈檢查每一個(gè)對(duì)象,直到找到該成員,或者抵達(dá)原型鏈的末端。原型鏈為繼承和共享開(kāi)辟了一條靈活的途徑。
你可能會(huì)問(wèn)的下一個(gè)問(wèn)題是:我該如何設(shè)置那些自定義對(duì)象的原型引用呢?例如前面所使用的點(diǎn)對(duì)象,如何才能將 add 方法添加到原型對(duì)象中,并從多個(gè)點(diǎn)對(duì)象中繼承方法呢?在回答這個(gè)問(wèn)題之前,我們需要看看函數(shù)。
有關(guān)函數(shù)
JavaScript 中的函數(shù)也是對(duì)象。這樣的表述帶來(lái)了幾個(gè)重要的結(jié)果,而我們并不會(huì)在本文中涉及所有的事項(xiàng)。這其中,能將一個(gè)函數(shù)賦值給一個(gè)變量,并且將一個(gè)函數(shù)作為參數(shù)傳遞給另一個(gè)函數(shù)的能力構(gòu)成了現(xiàn)代 JavaScript 編程表達(dá)的基本范式。
我們需要關(guān)注的是,函數(shù)本身就是對(duì)象,因此函數(shù)可以有自身的方法,屬性,并且引用一個(gè)原型對(duì)象。讓我們來(lái)討論下面的代碼的含義。
// 這將返回 true: typeof (Array) === "function" // 這樣的表達(dá)式也是: Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { }) // 這樣的表達(dá)式同樣: Array.prototype != null
代碼中的第一行證明, JavaScript 中的數(shù)組是函數(shù)。稍后我們將看到如何調(diào)用 Array 函數(shù)創(chuàng)建一個(gè)新的數(shù)組對(duì)象。下一行代碼,證明了 Array 對(duì)象使用與任何其他函數(shù)對(duì)象相同的原型,就像我們看到數(shù)組對(duì)象間共享相同的原型一樣。最后一行代碼證明了 Array 函數(shù)都有一個(gè) prototype 屬性,而這個(gè) prototype 屬性指向一個(gè)有效的對(duì)象。這個(gè) prototype 屬性十分重要。
JavaScript 中的每一個(gè)函數(shù)對(duì)象都有 prototype 屬性。千萬(wàn)不要混淆這個(gè) prototype 屬性的 __proto__ 屬性。他們用途并不相同,也不是指向同一個(gè)對(duì)象。
// 返回 true Object.getPrototypeOf(Array) != Array.prototype
Array.__proto__ 提供的是 數(shù)組原型 – 請(qǐng)把它當(dāng)作 Array 函數(shù)所繼承的對(duì)象。
而 Array.protoype,提供的的是 所有數(shù)組的原型對(duì)象。也就是說(shuō),它提供的是像 myArray 這樣數(shù)組對(duì)象的原型對(duì)象,也包含了所有數(shù)組將會(huì)繼承的方法。我們可以寫(xiě)一些代碼來(lái)證明這個(gè)事實(shí)。
// true Array.prototype == Object.getPrototypeOf(myArray) // 也是 true Array.prototype == Object.getPrototypeOf(yourArray);
我們也可以使用這項(xiàng)新知識(shí)重繪之前的示意圖。
圖 5
基于所知道的知識(shí),請(qǐng)想象創(chuàng)建一個(gè)新的對(duì)象,并讓新對(duì)象表現(xiàn)地像數(shù)組的過(guò)程。一種方法是使用下面的代碼。
// 創(chuàng)建一個(gè)新的空對(duì)象 var o = {}; // 繼承自同一個(gè)原型,一個(gè)數(shù)組對(duì)象 o.__proto__ = Array.prototype; // 現(xiàn)在我們可以調(diào)用數(shù)組的任何方法... o.push(3);
雖然這段代碼很有趣,也能工作,可問(wèn)題在于,并不是每一個(gè) JavaScript 環(huán)境都支持可寫(xiě)的 __proto__ 對(duì)象屬性。幸運(yùn)的是,JavaScript 確實(shí)有一個(gè)創(chuàng)建對(duì)象內(nèi)建的標(biāo)準(zhǔn)機(jī)制,只需要一個(gè)操作符,就可以創(chuàng)建新對(duì)象,并且設(shè)置新對(duì)象的 __proto__ 引用 – 那就是“new”操作符。
var o = new Array(); o.push(3);
JavaScript 中的 new 操作符有三個(gè)基本任務(wù)。首先,它創(chuàng)建新的空對(duì)象。接下來(lái),它將設(shè)置新對(duì)象的 __proto__ 屬性,以匹配所調(diào)用函數(shù)的原型屬性。最后,操作符調(diào)用函數(shù),將新對(duì)象作為“this”引用傳遞。如果要擴(kuò)展最后兩行代碼,就會(huì)變成如下情況:
var o = {}; o.__proto__ = Array.prototype; Array.call(o); o.push(3);
函數(shù)的 call 方法允許你在調(diào)用函數(shù)的情況下在函數(shù)內(nèi)部指定“this”所引用的對(duì)象。當(dāng)然,函數(shù)的作者在這種情況下需要實(shí)現(xiàn)這樣的函數(shù)。一旦作者創(chuàng)建了這樣的函數(shù),就可以將其稱之為構(gòu)造函數(shù)。
構(gòu)造函數(shù)
構(gòu)造函數(shù)和普通的函數(shù)一樣,但是具有以下兩個(gè)特殊性質(zhì)。
- 通常構(gòu)造函數(shù)的首字母是大寫(xiě)的(讓識(shí)別構(gòu)造函數(shù)變得更容易)。
- 構(gòu)造函數(shù)通常要和 new 操作符結(jié)合,用來(lái)構(gòu)造新對(duì)象。
Array 就是一個(gè)構(gòu)造函數(shù)的例子。Array 函數(shù)需要和 new 操作符一起使用,而且 Array 的首字母是大寫(xiě)的。JavaScript 將 Array 作為內(nèi)置函數(shù)包括在內(nèi),而任何人都可以寫(xiě)出自己的構(gòu)造函數(shù)。事實(shí)上,我們最后可以為先前創(chuàng)建的點(diǎn)對(duì)象編寫(xiě)出構(gòu)造函數(shù)。
var Point = function (x, y) { this.x = x; this.y = y; this.add = function (otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } } var p1 = new Point(3, 4); var p2 = new Point(8, 6); p1.add(p2);
在上面的代碼中,我們使用了 new 操作符和 Point 函數(shù)來(lái)構(gòu)造點(diǎn)對(duì)象,這個(gè)對(duì)象帶有 x 屬性和 y 屬性和一個(gè) add 方法。你可以將最后的結(jié)果想象成圖 6 的樣子。
圖 6
現(xiàn)在的問(wèn)題是我們的每個(gè)點(diǎn)對(duì)象中仍然有單獨(dú)的 add 方法。使用我們學(xué)到的原型和繼承的知識(shí),我們更希望將點(diǎn)對(duì)象的 add 方法從每個(gè)點(diǎn)實(shí)例中轉(zhuǎn)移到 Point.prototype 中。要達(dá)到繼承 add 方法的效果,我們所需要做的,就是修改 Point.prototype 對(duì)象。
var Point = function (x, y) { this.x = x; this.y = y; } Point.prototype.add = function (otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } var p1 = new Point(3, 4); var p2 = new Point(8, 6); p1.add(p2);
大功告成!我們剛剛在 JavaScript 中完成原型式的繼承模式!
圖 7
總結(jié)
我希望這篇文章能夠幫助你揭開(kāi) JavaScript 原型概念的神秘面紗。開(kāi)始看到的是原型怎樣讓一個(gè)對(duì)象從其他對(duì)象中繼承功能,然后看到怎樣結(jié)合 new 操作符和構(gòu)造函數(shù)來(lái)構(gòu)建對(duì)象。這里所提到的,只是開(kāi)啟對(duì)象原型力量和靈活性的第一步。本文鼓勵(lì)你自己發(fā)現(xiàn)學(xué)習(xí)有關(guān)原型和 JavaScript 語(yǔ)言的新信息。
同時(shí),請(qǐng)小心駕駛。你永遠(yuǎn)不會(huì)知道這些行駛在路上的車(chē)輛會(huì)從他們的原型繼承到什么(有缺陷)的技術(shù)。
原文鏈接: Script Junkie 翻譯: 伯樂(lè)在線 - 埃姆杰
相關(guān)文章
Nautil 中使用雙向數(shù)據(jù)綁定的實(shí)現(xiàn)
這篇文章主要介紹了Nautil 中使用雙向數(shù)據(jù)綁定的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10js 限制數(shù)字 js限制輸入實(shí)現(xiàn)代碼
在工作中經(jīng)常會(huì)遇到j(luò)s限制輸入方面的要求,本文將詳細(xì)介紹其實(shí)現(xiàn)原理,需要的朋友可以參考下2012-12-12JS閉包、作用域鏈、垃圾回收、內(nèi)存泄露相關(guān)知識(shí)小結(jié)
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù)。接下來(lái)通過(guò)本文給大家介紹JS閉包作用域鏈等相關(guān)知識(shí)小結(jié)(垃圾回收內(nèi)存泄露)的相關(guān)知識(shí),感興趣的朋友一起學(xué)習(xí)吧2016-05-05淺談JavaScript暫時(shí)性死區(qū)與垃圾回收機(jī)制
本文主要介紹了淺談JavaScript暫時(shí)性死區(qū)與垃圾回收機(jī)制,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05基于BootStrap Metronic開(kāi)發(fā)框架經(jīng)驗(yàn)小結(jié)【七】數(shù)據(jù)的導(dǎo)入、導(dǎo)出及附件的查看處理
在很多系統(tǒng)模塊里面,我們可能都需要進(jìn)行一定的數(shù)據(jù)交換處理,這樣可以很好的達(dá)到用戶操作體驗(yàn)感,接下來(lái)通過(guò)本文給大家介紹基于BootStrap Metronic開(kāi)發(fā)框架經(jīng)驗(yàn)小結(jié)【七】數(shù)據(jù)的導(dǎo)入、導(dǎo)出及附件的查看處理相關(guān)知識(shí),非常具有參考價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-05-05javascript寫(xiě)的一個(gè)模擬閱讀小說(shuō)的程序
這篇文章主要介紹了用javascript寫(xiě)了一個(gè)模擬閱讀小說(shuō)的程序,需要的朋友可以參考下2014-04-04HTML+CSS+JS實(shí)現(xiàn)的簡(jiǎn)單應(yīng)用小案例分享
這篇文章主要為大家分享四個(gè)用HTML+CSS+JS實(shí)現(xiàn)的簡(jiǎn)單應(yīng)用小案例,有:猜數(shù)字、表白墻、切換日夜間模式和待辦事項(xiàng),需要的可以參考一下2022-02-02JavaScript實(shí)現(xiàn)的DOM繪制柱狀圖效果示例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的DOM繪制柱狀圖效果,涉及javascript數(shù)值計(jì)算及頁(yè)面元素屬性動(dòng)態(tài)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-08-08js Select下拉列表框進(jìn)行多選、移除、交換內(nèi)容的具體實(shí)現(xiàn)方法
我們經(jīng)常會(huì)看到很多的網(wǎng)站會(huì)看到有下拉列表的內(nèi)容進(jìn)行直接增加與移除,下面我來(lái)介紹一款js Select下拉列表框進(jìn)行多選、移除、交換內(nèi)容實(shí)例2013-08-08