請在此暫時忘記之前學到的面向對象的一切知識。這裡只需要考慮賽車的情況。是的,就是賽車。
最近我正在觀看24 Hours of Le Mans,這是法國流行的一項賽事。最快的車被稱為Le Mans 原型車。這些車雖然是由“奧迪”或“標致”這些廠商製造的,可它們並不是你在街上或速公路上所見到的那類汽車。它們是專為參加高速耐力賽事而製造出來的。
廠家投入巨額資金,用於研發、設計、製造這些原型車,而工程師們總是努力嘗試將這項工程做到極致。他們在合金、生物燃料、制動技術、輪胎的化合物成分和安全特性上進行了各種實驗。隨著時間的推移,這些實驗中的某些技術經過反復改進,隨之進入到車輛的主流產品線中。你所駕駛車輛的某些技術,有可能是在賽車原型上第一次亮相的。
你也可以說,這些主流車輛繼承了來自賽車的技術原型。
到現在,我們就有討論JavaScript 中的原型和繼承問題的基礎了。它雖然並不像你在C++、Java 或C# 中了解的經典繼承模式一樣,但這種方式同樣強大,並且有可能會更加靈活。
JavaScript 中全是對象,這指的是傳統意義上的對象,也就是“一個包含了狀態和行為的單一實體”。例如,JavaScript 中的數組是含有數個值,並且包含push、reverse 和pop 方法的對象。
var myArray = [1, 2];myArray.push(3);myArray.reverse();myArray.pop();var length = myArray.length;
現在問題是,push 這樣的方法是從何而來的呢?我們前面提到的那些靜態語言使用“類語法”來定義對象的結構,但是JavaScript 是一個沒有“類語法”的語言,無法用Array“類”的語法來定義每個數組對象。而因為JavaScript 是動態語言,我們可以在實際需要的情況下,將方法任意放置到對像上。例如下面的代碼,就在二維空間中,定義了用來表示一個點的點對象,同時還定義了一個add 方法。
var point = { x : 10, y : 5, add: function(otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; }};但是上面的做法可擴展性並不好。我們需要確保每一個點對像都含有一個add 方法,同時也希望所有點對像都共享同一個add 方法的實現,而不是這個方法手工添加每一個點對像上。這就是原型發揮它作用的地方。
在JavaScript 中,每個對像都保持著一塊隱藏的狀態―― 一個對另一個對象的引用,也被稱作原型。我們之前創建的數組引用了一個原型對象,我們自行創建的點對像也是如此。上面說原型引用是隱藏的,但也有ECMAScript(JavaScript 的正式名稱)的實現可以通過一個對象的__proto__屬性(例如穀歌瀏覽器)訪問到這個原型引用。從概念上講,我們可以將對象當作類似於圖1 所表示的對象―― 原型的關係。
圖1
展望未來,開發者將能夠使用Object.getPrototypeOf 函數,代替__proto__屬性,取得對象原型的引用。在本文寫出的時候,已經可以在Google Chrome,FIrefox 和IE9 瀏覽器中使用Object.getPrototypeOf 函數。更多瀏覽器在未來會實現此功能,因為它已經是ECMAScript 標準的一部分了。我們可以使用下面的代碼,來證明我們建立的myArray 和點對象引用的是兩個不同的原型對象。
對於本文的其餘部分,我將交叉使用__proto__和Object.getPrototypeOf 函數,主要是因為__proto__ 在圖和句子中更容易識別。需要記住的是它(__proto__)不是標準,而Object.getPrototypeOf函數才是查看對象原型的推薦方法。
是什麼讓原型如此特別?
我們還沒有回答這個問題:數組中push 這樣的方法是從何而來的呢?答案是:它來源於myArray 原型對象。圖2 是Chrome 瀏覽器中腳本調試器的屏幕截圖。我們已經調用Object.getPrototypeOf 方法查看myArray 的原型對象。
圖2
注意myArray 的原型對像中有許多方法,包括那些在代碼示例中調用的push、pop 和reverse 方法。因此,原型對像中的確包括push 方法,但是myArray 方法如何引用到呢?
myArray.push(3);
了解其工作原理的第一步,是要認識到原型並不是特別的。原型只是普通的對象。可以給原型添加方法,屬性,並把他們當作其他JavaScript 對像一樣看待。然而,套用喬治・奧威爾的小說《動物農場》中“豬”的說法―― 所有的對象應當是平等的,但有些對象(遵守規則的)比其他人更加平等。
JavaScript 中的原型對象的確是特殊的,因為他們遵從以下規則。當我們告訴JavaScript 我們要調用一個對象的push 方法,或讀取對象的x 屬性時,運行時會首先查找對象本身。如果運行時找不到想要的東西,它就會循著__proto__ 引用和對象原型尋找該成員。當我們調用myArray 的push 方法時,JavaScript 並沒有在myArray 對像上發現push 方法,而是在myArray 的原型對像上找到了,於是JavaScript 調用此方法(見圖3)。
圖3
上面所描述的行為是指一個對象本身繼承了原型上的任何方法或屬性。 JavaScript 中其實不需要使用類語法也能實現繼承。就像從賽車原型上繼承了相應的技術的車,一個JavaScript 對像也可以從原型對像上繼承功能特性。
圖3 還展示了每個數組對象同時也可以維護自身的狀態和成員。在請求得到myArray 的length 屬性的情況下,JavaScript 會取得myArray 中length 屬性的值,而不會去讀取原型中的對應值。我們可以通過向對像上添加push 這樣的方法來“重寫”push 方法。這樣就會有效地隱藏原型中的push 方法實現。
JavaScript 中原型的真正神奇之處是多個對像如何維持對同一個原型對象的引用。例如,如果我們創建了這樣的兩個數組:
var myArray = [1, 2];var yourArray = [4, 5, 6];
那麼這兩個數組將共享同一個原型對象,而下面的代碼計算結果為true:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我們引用兩個數組對像上的push 方法,JavaScript 會去尋找原型上共享的push 方法。
圖4
JavaScript 中的原型對象提供繼承功能,同時也就實現了該方法實現的共享。原型也是鍊式的。換句話說,因為原型對像只是一個對象,所以一個原型對象可以維持到另一個原型對象的引用。如果你重新審視圖2 便可以看到,原型的__proto__ 屬性是一個指向另一個原型的非空值。當JavaScript 查找像push 方法這樣的成員時,它會循著原型引用鏈檢查每一個對象,直到找到該成員,或者抵達原型鏈的末端。原型鍊為繼承和共享開闢了一條靈活的途徑。
你可能會問的下一個問題是:我該如何設置那些自定義對象的原型引用呢?例如前面所使用的點對象,如何才能將add 方法添加到原型對像中,並從多個點對像中繼承方法呢?在回答這個問題之前,我們需要看看函數。
JavaScript 中的函數也是對象。這樣的表述帶來了幾個重要的結果,而我們並不會在本文中涉及所有的事項。這其中,能將一個函數賦值給一個變量,並且將一個函數作為參數傳遞給另一個函數的能力構成了現代JavaScript 編程表達的基本範式。
我們需要關注的是,函數本身就是對象,因此函數可以有自身的方法,屬性,並且引用一個原型對象。讓我們來討論下面的代碼的含義。
// 這將返回true:typeof (Array) === "function"// 這樣的表達式也是:Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })// 這樣的表達式同樣:Array.prototype != null代碼中的第一行證明, JavaScript 中的數組是函數。稍後我們將看到如何調用Array 函數創建一個新的數組對象。下一行代碼,證明了Array 對象使用與任何其他函數對象相同的原型,就像我們看到數組對象間共享相同的原型一樣。最後一行代碼證明了Array 函數都有一個prototype 屬性,而這個prototype 屬性指向一個有效的對象。這個prototype 屬性十分重要。
JavaScript 中的每一個函數對像都有prototype 屬性。千萬不要混淆這個prototype 屬性的__proto__ 屬性。他們用途並不相同,也不是指向同一個對象。
// 返回trueObject.getPrototypeOf(Array) != Array.prototype
Array.__proto__ 提供的是數組原型請把它當作Array 函數所繼承的對象。
而Array.protoype,提供的的是所有數組的原型對象。也就是說,它提供的是像myArray 這樣數組對象的原型對象,也包含了所有數組將會繼承的方法。我們可以寫一些代碼來證明這個事實。
// trueArray.prototype == Object.getPrototypeOf(myArray)// 也是trueArray.prototype == Object.getPrototypeOf(yourArray);
我們也可以使用這項新知識重繪之前的示意圖。
圖5
基於所知道的知識,請想像創建一個新的對象,並讓新對象表現地像數組的過程。一種方法是使用下面的代碼。
// 創建一個新的空對象var o = {};// 繼承自同一個原型,一個數組對象o.__proto__ = Array.prototype;// 現在我們可以調用數組的任何方法...o.push(3);雖然這段代碼很有趣,也能工作,可問題在於,並不是每一個JavaScript 環境都支持可寫的__proto__ 對象屬性。幸運的是,JavaScript 確實有一個創建對象內建的標準機制,只需要一個操作符,就可以創建新對象,並且設置新對象的__proto__ 引用那就是“new”操作符。
var o = new Array();o.push(3);
JavaScript 中的new 操作符有三個基本任務。首先,它創建新的空對象。接下來,它將設置新對象的__proto__ 屬性,以匹配所調用函數的原型屬性。最後,操作符調用函數,將新對像作為“this”引用傳遞。如果要擴展最後兩行代碼,就會變成如下情況:
var o = {};o.__proto__ = Array.prototype;Array.call(o);o.push(3);函數的call 方法允許你在調用函數的情況下在函數內部指定“this”所引用的對象。當然,函數的作者在這種情況下需要實現這樣的函數。一旦作者創建了這樣的函數,就可以將其稱之為構造函數。
構造函數
構造函數和普通的函數一樣,但是具有以下兩個特殊性質。
Array 就是一個構造函數的例子。 Array 函數需要和new 操作符一起使用,而且Array 的首字母是大寫的。 JavaScript 將Array 作為內置函數包括在內,而任何人都可以寫出自己的構造函數。事實上,我們最後可以為先前創建的點對象編寫出構造函數。
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 函數來構造點對象,這個對象帶有x 屬性和y 屬性和一個add 方法。你可以將最後的結果想像成圖6 的樣子。
圖6
現在的問題是我們的每個點對像中仍然有單獨的add 方法。使用我們學到的原型和繼承的知識,我們更希望將點對象的add 方法從每個點實例中轉移到Point.prototype 中。要達到繼承add 方法的效果,我們所需要做的,就是修改Point.prototype 對象。
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
總結
我希望這篇文章能夠幫助你揭開JavaScript 原型概念的神秘面紗。開始看到的是原型怎樣讓一個對像從其他對像中繼承功能,然後看到怎樣結合new 操作符和構造函數來構建對象。這裡所提到的,只是開啟對象原型力量和靈活性的第一步。本文鼓勵你自己發現學習有關原型和JavaScript 語言的新信息。
同時,請小心駕駛。你永遠不會知道這些行駛在路上的車輛會從他們的原型繼承到什麼(有缺陷)的技術。
原文鏈接: Script Junkie翻譯: 伯樂在線- 埃姆傑