JavaScript中的原型和繼承

請在此暫時忘記以前學到的面向對象的一切知識。這裏只須要考慮賽車的狀況。是的,就是賽車。javascript

最近我正在觀看 24 Hours of Le Mans ,這是法國流行的一項賽事。最快的車被稱爲 Le Mans 原型車。這些車雖然是由「奧迪」或「標緻」這些廠商製造的,可它們並非你在街上或速公路上所見到的那類汽車。它們是專爲參加高速耐力賽事而製造出來的。java

廠家投入鉅額資金,用於研發、設計、製造這些原型車,而工程師們老是努力嘗試將這項工程作到極致。他們在合金、生物燃料、制動技術、輪胎的化合物成分和安全特性上進行了各類實驗。隨着時間的推移,這些實驗中的某些技術通過反覆改進,隨之進入到車輛的主流產品線中。你所駕駛車輛的某些技術,有多是在賽車原型上第一次亮相的。編程

你也能夠說,這些主流車輛繼承了來自賽車的技術原型數組

到如今,咱們就有討論 JavaScript 中的原型和繼承問題的基礎了。它雖然並不像你在 C++、Java 或 C# 中瞭解的經典繼承模式同樣,但這種方式一樣強大,而且有可能會更加靈活。瀏覽器

有關對象和類

JavaScript 中全是對象,這指的是傳統意義上的對象,也就是「一個包含了狀態和行爲的單一實體」。例如,JavaScript 中的數組是含有數個值,而且包含 push、reverse 和 pop 方法的對象。安全

1
2
3
4
5
var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;

如今問題是,push 這樣的方法是從何而來的呢?咱們前面提到的那些靜態語言使用「類語法」來定義對象的結構,可是 JavaScript 是一個沒有「類語法」的語言,沒法用 Array「類」的語法來定義每一個數組對象。而由於 JavaScript 是動態語言,咱們能夠在實際須要的狀況下,將方法任意放置到對象上。例以下面的代碼,就在二維空間中,定義了用來表示一個點的點對象,同時還定義了一個 add 方法。函數

1
2
3
4
5
6
7
8
var point = {
     x : 10,
     y : 5,
     add: function (otherPoint) {
         this .x += otherPoint.x;
         this .y += otherPoint.y;
     }
};

可是上面的作法可擴展性並很差。咱們須要確保每個點對象都含有一個 add 方法,同時也但願全部點對象都共享同一個 add 方法的實現,而不是這個方法手工添加每個點對象上。這就是原型發揮它做用的地方。學習

有關原型

在 JavaScript 中,每一個對象都保持着一塊隱藏的狀態 —— 一個對另外一個對象的引用,也被稱做原型。咱們以前建立的數組引用了一個原型對象,咱們自行建立的點對象也是如此。上面說原型引用是隱藏的,但也有 ECMAScript(JavaScript 的正式名稱)的實現能夠經過一個對象的__proto__屬性(例如谷歌瀏覽器)訪問到這個原型引用。從概念上講,咱們能夠將對象看成相似於 圖1 所表示的對象 —— 原型的關係。this

 

 1spa

展望將來,開發者將可以使用 Object.getPrototypeOf 函數,代替__proto__屬性,取得對象原型的引用。在本文寫出的時候,已經能夠在 Google Chrome,FIrefox 和 IE9 瀏覽器中使用 Object.getPrototypeOf 函數。更多瀏覽器在將來會實現此功能,由於它已是 ECMAScript 標準的一部分了。咱們可使用下面的代碼,來證實咱們創建的 myArray 和點對象引用的是兩個不一樣的原型對象。

  1. Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);

對於本文的其他部分,我將交叉使用 __proto__和Object.getPrototypeOf 函數,主要是由於 __proto__ 在圖和句子中更容易識別。須要記住的是它(__proto__)不是標準,而Object.getPrototypeOf 函數纔是查看對象原型的推薦方法。

是什麼讓原型如此特別?

咱們尚未回答這個問題:數組中 push 這樣的方法是從何而來的呢?答案是:它來源於 myArray 原型對象。圖 2 是 Chrome 瀏覽器中腳本調試器的屏幕截圖。咱們已經調用 Object.getPrototypeOf 方法查看 myArray 的原型對象。

 

 2

注意 myArray 的原型對象中有許多方法,包括那些在代碼示例中調用的 push、pop 和 reverse 方法。所以,原型對象中的確包括 push 方法,可是 myArray 方法如何引用到呢?

1
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 中原型的真正神奇之處是多個對象如何維持對同一個原型對象的引用。例如,若是咱們建立了這樣的兩個數組:

1
2
var myArray = [1, 2];
var yourArray = [4, 5, 6];

那麼這兩個數組將共享同一個原型對象,而下面的代碼計算結果爲 true:

1
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);

若是咱們引用兩個數組對象上的 push 方法,JavaScript 會去尋找原型上共享的 push 方法。

 4

JavaScript 中的原型對象提供繼承功能,同時也就實現了該方法實現的共享。原型也是鏈式的。換句話說,由於原型對象只是一個對象,因此一個原型對象能夠維持到另外一個原型對象的引用。若是你從新審視圖 2 即可以看到,原型的 __proto__ 屬性是一個指向另外一個原型的非空值。當 JavaScript 查找像 push 方法這樣的成員時,它會循着原型引用鏈檢查每個對象,直到找到該成員,或者抵達原型鏈的末端。原型鏈爲繼承和共享開闢了一條靈活的途徑。

你可能會問的下一個問題是:我該如何設置那些自定義對象的原型引用呢?例如前面所使用的點對象,如何才能將 add 方法添加到原型對象中,並從多個點對象中繼承方法呢?在回答這個問題以前,咱們須要看看函數。

有關函數

JavaScript 中的函數也是對象。這樣的表述帶來了幾個重要的結果,而咱們並不會在本文中涉及全部的事項。這其中,能將一個函數賦值給一個變量,而且將一個函數做爲參數傳遞給另外一個函數的能力構成了現代 JavaScript 編程表達的基本範式。

咱們須要關注的是,函數自己就是對象,所以函數能夠有自身的方法,屬性,而且引用一個原型對象。讓咱們來討論下面的代碼的含義。

1
2
3
4
5
6
// 這將返回 true:
typeof (Array) === "function"
// 這樣的表達式也是:
Object.getPrototypeOf(Array) === Object.getPrototypeOf( function () { })
// 這樣的表達式一樣:
Array.prototype != null

代碼中的第一行證實, JavaScript 中的數組是函數。稍後咱們將看到如何調用 Array 函數建立一個新的數組對象。下一行代碼,證實了 Array 對象使用與任何其餘函數對象相同的原型,就像咱們看到數組對象間共享相同的原型同樣。最後一行代碼證實了 Array 函數都有一個 prototype 屬性,而這個 prototype 屬性指向一個有效的對象。這個 prototype 屬性十分重要。

JavaScript 中的每個函數對象都有 prototype 屬性。千萬不要混淆這個 prototype 屬性的 __proto__ 屬性。他們用途並不相同,也不是指向同一個對象。

1
2
// 返回 true
Object.getPrototypeOf(Array) != Array.prototype

Array.__proto__ 提供的是 數組原型 – 請把它看成 Array 函數所繼承的對象。

而 Array.protoype,提供的的是 全部數組的原型對象。也就是說,它提供的是像 myArray 這樣數組對象的原型對象,也包含了全部數組將會繼承的方法。咱們能夠寫一些代碼來證實這個事實。

1
2
3
4
// true
Array.prototype == Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype == Object.getPrototypeOf(yourArray);

咱們也可使用這項新知識重繪以前的示意圖。

 5

基於所知道的知識,請想象建立一個新的對象,並讓新對象表現地像數組的過程。一種方法是使用下面的代碼。

1
2
3
4
5
6
// 建立一個新的空對象
var o = {};
// 繼承自同一個原型,一個數組對象
o.__proto__ = Array.prototype;
// 如今咱們能夠調用數組的任何方法...
o.push(3);

雖然這段代碼頗有趣,也能工做,可問題在於,並非每個 JavaScript 環境都支持可寫的 __proto__ 對象屬性。幸運的是,JavaScript 確實有一個建立對象內建的標準機制,只須要一個操做符,就能夠建立新對象,而且設置新對象的 __proto__ 引用 – 那就是「new」操做符。

1
2
var o = new Array();
o.push(3);

JavaScript 中的 new 操做符有三個基本任務。首先,它建立新的空對象。接下來,它將設置新對象的 __proto__ 屬性,以匹配所調用函數的原型屬性。最後,操做符調用函數,將新對象做爲「this」引用傳遞。若是要擴展最後兩行代碼,就會變成以下狀況:

1
2
3
4
var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);

函數的 call 方法容許你在調用函數的狀況下在函數內部指定「this」所引用的對象。固然,函數的做者在這種狀況下須要實現這樣的函數。一旦做者建立了這樣的函數,就能夠將其稱之爲構造函數。

構造函數

構造函數和普通的函數同樣,可是具備如下兩個特殊性質。

  1. 一般構造函數的首字母是大寫的(讓識別構造函數變得更容易)。
  2. 構造函數一般要和 new 操做符結合,用來構造新對象。

Array 就是一個構造函數的例子。Array 函數須要和 new 操做符一塊兒使用,並且 Array 的首字母是大寫的。JavaScript 將 Array 做爲內置函數包括在內,而任何人均可以寫出本身的構造函數。事實上,咱們最後能夠爲先前建立的點對象編寫出構造函數。

1
2
3
4
5
6
7
8
9
10
11
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 對象。

1
2
3
4
5
6
7
8
9
10
11
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   翻譯: 伯樂在線 埃姆傑
譯文連接: http://blog.jobbole.com/66441/

相關文章
相關標籤/搜索