到現(xiàn)在,我們就有討論 JavaScript 中的原型和繼承問題的基礎(chǔ)了。它雖然并不像你在 C++、Java 或 C# 中了解的經(jīng)典繼承模式一樣,但這種方式同樣強大,并且有可能會更加靈活。
有關(guān)對象和類
JavaScript 中全是對象,這指的是傳統(tǒng)意義上的對象,也就是“一個包含了狀態(tài)和行為的單一實體”。例如,JavaScript 中的數(shù)組是含有數(shù)個值,并且包含 push、reverse 和 pop 方法的對象。
var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;
現(xiàn)在問題是,push 這樣的方法是從何而來的呢?我們前面提到的那些靜態(tài)語言使用“類語法”來定義對象的結(jié)構(gòu),但是 JavaScript 是一個沒有“類語法”的語言,無法用 Array“類”的語法來定義每個數(shù)組對象。而因為 JavaScript 是動態(tài)語言,我們可以在實際需要的情況下,將方法任意放置到對象上。例如下面的代碼,就在二維空間中,定義了用來表示一個點的點對象,同時還定義了一個 add 方法。
var point = {
x : 10,
y : 5,
add: function(otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
};
但是上面的做法可擴展性并不好。我們需要確保每一個點對象都含有一個 add 方法,同時也希望所有點對象都共享同一個 add 方法的實現(xiàn),而不是這個方法手工添加每一個點對象上。這就是原型發(fā)揮它作用的地方。
有關(guān)原型
在 JavaScript 中,每個對象都保持著一塊隱藏的狀態(tài) —— 一個對另一個對象的引用,也被稱作原型。我們之前創(chuàng)建的數(shù)組引用了一個原型對象,我們自行創(chuàng)建的點對象也是如此。上面說原型引用是隱藏的,但也有 ECMAScript(JavaScript 的正式名稱)的實現(xiàn)可以通過一個對象的__proto__屬性(例如谷歌瀏覽器)訪問到這個原型引用。從概念上講,我們可以將對象當作類似于 圖1 所表示的對象 —— 原型的關(guān)系。
圖1
展望未來,開發(fā)者將能夠使用 Object.getPrototypeOf 函數(shù),代替__proto__屬性,取得對象原型的引用。在本文寫出的時候,已經(jīng)可以在 Google Chrome,F(xiàn)Irefox 和 IE9 瀏覽器中使用 Object.getPrototypeOf 函數(shù)。更多瀏覽器在未來會實現(xiàn)此功能,因為它已經(jīng)是 ECMAScript 標準的一部分了。我們可以使用下面的代碼,來證明我們建立的 myArray 和點對象引用的是兩個不同的原型對象。
Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);
對于本文的其余部分,我將交叉使用 __proto__和Object.getPrototypeOf 函數(shù),主要是因為 __proto__ 在圖和句子中更容易識別。需要記住的是它(__proto__)不是標準,而 Object.getPrototypeOf 函數(shù)才是查看對象原型的推薦方法。
是什么讓原型如此特別?
我們還沒有回答這個問題:數(shù)組中 push 這樣的方法是從何而來的呢?答案是:它來源于 myArray 原型對象。圖 2 是 Chrome 瀏覽器中腳本調(diào)試器的屏幕截圖。我們已經(jīng)調(diào)用 Object.getPrototypeOf 方法查看 myArray 的原型對象。
圖 2
注意 myArray 的原型對象中有許多方法,包括那些在代碼示例中調(diào)用的 push、pop 和 reverse 方法。因此,原型對象中的確包括 push 方法,但是 myArray 方法如何引用到呢?
myArray.push(3);
了解其工作原理的第一步,是要認識到原型并不是特別的。原型只是普通的對象??梢越o原型添加方法,屬性,并把他們當作其他 JavaScript 對象一樣看待。然而,套用喬治·奧威爾的小說《動物農(nóng)場》中“豬”的說法 —— 所有的對象應(yīng)當是平等的,但有些對象(遵守規(guī)則的)比其他人更加平等。
JavaScript 中的原型對象的確是特殊的,因為他們遵從以下規(guī)則。當我們告訴 JavaScript 我們要調(diào)用一個對象的 push 方法,或讀取對象的 x 屬性時,運行時會首先查找對象本身。如果運行時找不到想要的東西,它就會循著 __proto__ 引用和對象原型尋找該成員。當我們 調(diào)用 myArray 的 push 方法時,JavaScript 并沒有在 myArray 對象上發(fā)現(xiàn) push 方法,而是在 myArray 的原型對象上找到了,于是 JavaScript 調(diào)用此方法(見圖 3)。
圖3上面所描述的行為是指一個對象本身繼承了原型上的任何方法或?qū)傩浴avaScript 中其實不需要使用類語法也能實現(xiàn)繼承。就像從賽車原型上繼承了相應(yīng)的技術(shù)的車,一個 JavaScript 對象也可以從原型對象上繼承功能特性。
圖 3 還展示了每個數(shù)組對象同時也可以維護自身的狀態(tài)和成員。在請求得到 myArray 的 length 屬性的情況下,JavaScript 會取得 myArray 中 length 屬性的值,而不會去讀取原型中的對應(yīng)值。我們可以通過向?qū)ο笊咸砑?push 這樣的方法來“重寫”push 方法。這樣就會有效地隱藏原型中的 push 方法實現(xiàn)。
共享原型
JavaScript 中原型的真正神奇之處是多個對象如何維持對同一個原型對象的引用。例如,如果我們創(chuàng)建了這樣的兩個數(shù)組:
var myArray = [1, 2];
var yourArray = [4, 5, 6];
那么這兩個數(shù)組將共享同一個原型對象,而下面的代碼計算結(jié)果為 true:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我們引用兩個數(shù)組對象上的 push 方法,JavaScript 會去尋找原型上共享的 push 方法。
圖 4
JavaScript 中的原型對象提供繼承功能,同時也就實現(xiàn)了該方法實現(xiàn)的共享。原型也是鏈式的。換句話說,因為原型對象只是一個對象,所以一個原型對象可以維持到另一個原型對象的引用。如果你重新審視圖 2 便可以看到,原型的 __proto__ 屬性是一個指向另一個原型的非空值。當 JavaScript 查找像 push 方法這樣的成員時,它會循著原型引用鏈檢查每一個對象,直到找到該成員,或者抵達原型鏈的末端。原型鏈為繼承和共享開辟了一條靈活的途徑。
你可能會問的下一個問題是:我該如何設(shè)置那些自定義對象的原型引用呢?例如前面所使用的點對象,如何才能將 add 方法添加到原型對象中,并從多個點對象中繼承方法呢?在回答這個問題之前,我們需要看看函數(shù)。
有關(guān)函數(shù)
JavaScript 中的函數(shù)也是對象。這樣的表述帶來了幾個重要的結(jié)果,而我們并不會在本文中涉及所有的事項。這其中,能將一個函數(shù)賦值給一個變量,并且將一個函數(shù)作為參數(shù)傳遞給另一個函數(shù)的能力構(gòu)成了現(xiàn)代 JavaScript 編程表達的基本范式。
我們需要關(guān)注的是,函數(shù)本身就是對象,因此函數(shù)可以有自身的方法,屬性,并且引用一個原型對象。讓我們來討論下面的代碼的含義。
// 這將返回 true:
typeof (Array) === "function"
// 這樣的表達式也是:
Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })
// 這樣的表達式同樣:
Array.prototype != null
代碼中的第一行證明, JavaScript 中的數(shù)組是函數(shù)。稍后我們將看到如何調(diào)用 Array 函數(shù)創(chuàng)建一個新的數(shù)組對象。下一行代碼,證明了 Array 對象使用與任何其他函數(shù)對象相同的原型,就像我們看到數(shù)組對象間共享相同的原型一樣。最后一行代碼證明了 Array 函數(shù)都有一個 prototype 屬性,而這個 prototype 屬性指向一個有效的對象。這個 prototype 屬性十分重要。
JavaScript 中的每一個函數(shù)對象都有 prototype 屬性。千萬不要混淆這個 prototype 屬性的 __proto__ 屬性。他們用途并不相同,也不是指向同一個對象。
// 返回 true
Object.getPrototypeOf(Array) != Array.prototype
Array.__proto__ 提供的是 數(shù)組原型 – 請把它當作 Array 函數(shù)所繼承的對象。
而 Array.protoype,提供的的是 所有數(shù)組的原型對象。也就是說,它提供的是像 myArray 這樣數(shù)組對象的原型對象,也包含了所有數(shù)組將會繼承的方法。我們可以寫一些代碼來證明這個事實。
// true
Array.prototype == Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype == Object.getPrototypeOf(yourArray);
我們也可以使用這項新知識重繪之前的示意圖。
圖 5
基于所知道的知識,請想象創(chuàng)建一個新的對象,并讓新對象表現(xiàn)地像數(shù)組的過程。一種方法是使用下面的代碼。
// 創(chuàng)建一個新的空對象
var o = {};
// 繼承自同一個原型,一個數(shù)組對象
o.__proto__ = Array.prototype;
// 現(xiàn)在我們可以調(diào)用數(shù)組的任何方法...
o.push(3);
雖然這段代碼很有趣,也能工作,可問題在于,并不是每一個 JavaScript 環(huán)境都支持可寫的 __proto__ 對象屬性。幸運的是,JavaScript 確實有一個創(chuàng)建對象內(nèi)建的標準機制,只需要一個操作符,就可以創(chuàng)建新對象,并且設(shè)置新對象的 __proto__ 引用 – 那就是“new”操作符。
var o = new Array();
o.push(3);
JavaScript 中的 new 操作符有三個基本任務(wù)。首先,它創(chuàng)建新的空對象。接下來,它將設(shè)置新對象的 __proto__ 屬性,以匹配所調(diào)用函數(shù)的原型屬性。最后,操作符調(diào)用函數(shù),將新對象作為“this”引用傳遞。如果要擴展最后兩行代碼,就會變成如下情況:
var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);
函數(shù)的 call 方法允許你在調(diào)用函數(shù)的情況下在函數(shù)內(nèi)部指定“this”所引用的對象。當然,函數(shù)的作者在這種情況下需要實現(xiàn)這樣的函數(shù)。一旦作者創(chuàng)建了這樣的函數(shù),就可以將其稱之為構(gòu)造函數(shù)。
構(gòu)造函數(shù)
構(gòu)造函數(shù)和普通的函數(shù)一樣,但是具有以下兩個特殊性質(zhì)。
通常構(gòu)造函數(shù)的首字母是大寫的(讓識別構(gòu)造函數(shù)變得更容易)。
構(gòu)造函數(shù)通常要和 new 操作符結(jié)合,用來構(gòu)造新對象。
Array 就是一個構(gòu)造函數(shù)的例子。Array 函數(shù)需要和 new 操作符一起使用,而且 Array 的首字母是大寫的。JavaScript 將 Array 作為內(nèi)置函數(shù)包括在內(nèi),而任何人都可以寫出自己的構(gòu)造函數(shù)。事實上,我們最后可以為先前創(chuàng)建的點對象編寫出構(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ù)來構(gòu)造點對象,這個對象帶有 x 屬性和 y 屬性和一個 add 方法。你可以將最后的結(jié)果想象成圖 6 的樣子。
圖 6
現(xiàn)在的問題是我們的每個點對象中仍然有單獨的 add 方法。使用我們學(xué)到的原型和繼承的知識,我們更希望將點對象的 add 方法從每個點實例中轉(zhuǎn)移到 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
總結(jié)
我希望這篇文章能夠幫助你揭開 JavaScript 原型概念的神秘面紗。開始看到的是原型怎樣讓一個對象從其他對象中繼承功能,然后看到怎樣結(jié)合 new 操作符和構(gòu)造函數(shù)來構(gòu)建對象。這里所提到的,只是開啟對象原型力量和靈活性的第一步。本文鼓勵你自己發(fā)現(xiàn)學(xué)習(xí)有關(guān)原型和 JavaScript 語言的新信息。
同時,請小心駕駛。你永遠不會知道這些行駛在路上的車輛會從他們的原型繼承到什么(有缺陷)的技術(shù)。
更多信息請查看IT技術(shù)專欄