本文翻譯自微軟的牛人Scott Allen Prototypes and Inheritance in JavaScript ,本文對到底什麼是Prototype和為什麼透過Prototype能實現繼承做了詳細的分析和闡述,是理解JS OO 的佳作之一。翻譯不好的地方望大家修改補充。
JS從傳統觀點來說是物件導向的語言。屬性、行為組合成一個物件。比如說,JS中的array就是由屬性和方法(如push、reverse、pop 等)組合成的物件。
問題是:這些方法(如push)從哪裡來的?有些靜態語言(例如JAVA)用class來定義一個物件的結構。但是JS是沒有」class"(classless)的語言,沒有一個叫做「Array」的類別定義了這些方法給每個array去繼承。因為JS是動態的,我們可以在需要的時候隨意的往物件中加入方法。例如,下面的程式碼定義了一個對象,表示二維空間中的座標,裡面有一個add方法。
我們想讓每個point物件都有一個add方法。我們也希望所有的poin物件共用一個add方法,而不必把add方法加到所有的point物件當中。這就需要讓prototype登場了。
JS中每個物件都有一個隱含的屬性(state)-對另一個物件的引用,稱為物件的prototype.我們上面建立的array和point當然也都含有各自prototype的引用。 prototype引用是隱含的,但是它是ECMAScript已實現的,允許我們使用物件的_proto_(在Chrome中)屬性來取得它。從概念上理解我們可以認為物件和prototype的關係就像下圖所表示的:
Object.getPrototype .getPrototypeOf(myArray);
在文章接下來的部分,我也會使用到_proto_,主要是因為_proto_在圖示和句子裡比較直觀。但要記住這不是規範的,Object.getPrototypeOf才是用來取得物件prototype的推薦方法。
2.1是什麼使Prototypes如此特別?
我們已經知道了array的push方法來自myArray的prototype物件。圖2是Chrome中的一個截圖,我們呼叫Object.getPrototypeOf方法來取得myArray的prototype物件。
圖2
注意到myArray的prototype物件裡包含了很多方法,例如push、pop還有reverse這些我們在開頭程式碼裡使用過的。 prototype物件才是push方法的唯一擁有者,但這個方法是如何透過myArray呼叫的呢?
myArray.push(3);
myArray.push(3);
想明白它是怎麼實現的,第一步是認清Protytype一點兒不特殊。 Prototype就是一些物件。我們可以為這些物件添加方法、屬性,像其他任何的JS物件一樣。但同時Prototype也是一個特殊的物件。
Prototype的特殊是因為下列規則:當我們通知JS我們想要在一個物件上呼叫push方法或是讀取某個屬性的時候,解釋器(runtime)首先去尋找這個物件本身的方法或屬性。如果解釋器沒有找到該方法(或屬性)就會沿著_proto_引用去尋找物件的prototype中的各個成員。當我們在myArray中呼叫push方法,JS沒有在myArray物件中找到push,但是在myArray的prototype物件中找到了push,即呼叫了該push方法(圖3)。
圖 3
我所描述的這種行為本質上就是物件本身繼承了 它的prototype中的所有方法和屬性。我們在JS中不需要用class來實現這種繼承關係。即,一個JS物件從它的prototype中繼承一個特性。
圖3也告訴我們每個array物件都能維護自己的狀態(state)和成員。如果我們需要myArray的length屬性,JS將從myArray找到length的值而不會去prototype中去找。我們能運用這個特性來「override"一個方法,即,將需覆蓋的方法(像push)放到myArray自己的物件中。這樣做就可以有效的將prototype中的push方法隱藏掉。
3.共享Prototype
Prototype在JS中真正神奇的地方是多個物件能引用同一個prototype物件。例如,我們建立兩個陣列:
複製程式碼
程式碼如下:
var mymyray = [var mymy 1, 2];var yourArray = [4, 5, 6];
這兩個數組將共享一個相同的prototype對象,下面的程式碼將返回true
複製程式碼
程式碼如下:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArtotypeOf(your);
如果我們在兩個陣列中呼叫push方法,JS將呼叫他們共同的prototype中的push。
Prototype物件在JS中給我們這種繼承的特性,它們也允許我們共享方法的實作。 Prototype也是鍊式的。換句話說,prototype是一個對象,那麼prototype對像也可以擁有一個指向別的prototype對象的引用。從圖2可以看到prototype的_proto_屬性是一個不為null的值也指向另外一個prototype.當JS開始尋找成員變數的時候,例如push方法,它將沿著這些prototype的引用檢查每一個物件直到找到這個物件或達到鏈的尾部。這種鏈的方式更增加了JS中繼承和共享的靈活性。
接下來你也許會問:我怎麼設定自訂物件的prototype引用?例如,我們之前建立過的物件point,我們怎麼加入一個add方法到prototype物件中,讓所有的point物件都能繼承它?在我們回答這個問題之前,我們先了解一下JS中的函數.
4.關於Funciton
函數在JS中同樣也是物件。函數在JS中有很多重要的特性,在這篇文章中我們不能一一列舉。但像把一個函數賦值給一個變數或是將一個函數當作另一個函數的參數在現今的JS程式設計中是很基礎的方式。
我們需要關注的是:因為函數是對象,所以它擁有方法、屬性和一個prototype對象的引用。讓我們一起討論下面程式碼的意思:
// this will return true:
typeof (Array) === "function"
// and so will this:
Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })
// and this, too:
Array.prototype != null
第一行程式碼證明Array在JS中是一個函數。待會兒我們將看到怎樣呼叫Array函數來建立一個新的array物件。
第二行程式碼證明Array物件和function物件引用相同的prototype,就像我們之前看到的所有的array物件共用一個prototype。
最後一行證明Array函數有一個prototype屬性。千萬不要將這個prototype屬性和_proto_屬性混淆了。它們的使用目的和指向的物件都不相同。
// true
Array.prototype == == .getPrototypeOf(myArray)
// also true
Array.prototype == Object.getPrototypeOf(yourArray);
我們用新學的知識重畫之前的圖片:🎜 >
其中一個方法是:
// create a new, empty object
var o = {};
// inherit from the same prototype as an array object
o.__proto__ = Array.prototype;
// now we can invarray method can invarray method ....
o.push(3);
儘管上面的程式碼看起來不錯,但問題是不是每個JS的環境都支援物件的_proto_屬性。幸運的是,JS內建一個標準的機制用來建立新物件同時設定物件的_proto_屬性,這就是「new」操作符。
var o = new Array();
var o = new Array();
o
o
「new」操作符在JS中有三個重要的任務:首先,它建立一個新的空物件。接著,它設定這個新物件的_proto_屬性指向呼叫函數的prototype屬性。最後,執行呼叫函數同時把「this」指標指向新的物件。如果我們把上面的兩行程式碼展開,將得到以下的程式碼:
複製程式碼
程式碼如下:
var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);
函數的「call」方法允許你呼叫一個函數同時指定這個函數裡面的"this"指向傳入的新物件。當然,我們也想透過上面的方法來創造我們自己的物件來實現物件的繼承,而這種函數就是我們所熟知的──建構子。
5.建構子
建構子是一個有兩個獨特標識的普通JS函數物件:
1.首字母大寫(容易辨識)。
2.用new運算元連接來建構新物件。
Array就是一個建構子-Array函式用new連接、首字母大寫。 JS中的Array函數是內建的,但任何人都可以建立自己的建構子。事實上,我們終於到了該為point物件來建立一個建構函式的時候了。 複製程式碼
程式碼如下:
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函數來建構一個point物件。在記憶體中你可以把最終的結果想成圖6所表示的樣子。
圖 6
現在的問題是,add方法存在於每一個point物件中。鑑於我們對prototype的了解,把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) ;
好了!我們已經用prototype實作了JS中的繼承!
6.總結 希望能透過這篇文章讓你能撥開prototype的迷霧。當然這只是功能強大又靈活的prototype的入門。更多的關於prototype的知識還是希望讀者能夠自己去探索和發現。