一、前言
本文翻譯自微軟的牛人Scott Allen Prototypes and Inheritance in JavaScript ,本文對到底什麼是Prototype和為什麼通過Prototype能實現繼承做了詳細的分析和闡述,是理解JS OO 的佳作之一。翻譯不好的地方望大家修改補充。
二、本文
JavaScript中的物件導向不同於其他語言,在學習前最好忘掉你所熟知的物件導向的概念。JS中的OO更強大、更值得討論(arguably)、更靈活。
1.類和對象
JS從傳統觀點來說是物件導向的語言。屬性、行為組合成一個對象。比如說,JS中的array就是由屬性和方法(如push、reverse、pop 等)組合成的對象。 複製代碼 代碼如下:var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;
問題是:這些方法(如push)從哪裡來的?一些靜態語言(比如JAVA)用class來定義一個對象的結構。但是JS是沒有”class"(classless)的語言,沒有一個叫做“Array"的類定義了這些方法給每個array去繼承。因為JS是動態,我們可以在需要的時候隨意的往對象中添加方法。例如,下面的代碼定義了一個對象,該對象表示二維空間中的座標,裡面有一個add方法。 複製代碼 代碼如下:var point = {
x : 10,
y : 5,
add: function(otherPoint)
{
this.x = otherPoint.x;
this.y = otherPoint.y;
}
};
我們想讓每一個point對象都有一個add方法。我們也希望所有的poin對象共用一個add方法,而不必把add方法加到所有的point對象當中。這就需要讓prototype登場了。
2.關於Prototypes
JS中每一個對象都有一個隱含的屬性(state)——對另一個對象的引用,稱為對象的prototype.我們上面建立的array和point當然也都含有各自prototype的引用。prototype引用是隱含的,但是它是ECMAScript已實現的,允許我們使用對象的_proto_(在Chrome中)屬性來擷取它。從概念上理解我們可以認為對象和prototype的關係就像所表示的:
作為開發人員,我們將用Object.getPrototypeOf 函數來替代_proto_屬性來查看對象的prototype引用。在寫這篇文章的時候,Object.getPrototypeOf這個函數已經在Chrome,firefox,還有IE9中提供了支援。在未來還會有更多的瀏覽器來支援這一特性,這已經是ECMAScript的標準之一。我們可以用以下代碼來證明myArray和我們之前建立的point對象確實引用了兩個不同的prototype對象。
複製代碼 代碼如下:Object.getPrototypeOf(point) != Object.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);
要想明白它是怎樣實現的,第一步是認清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 myArray = [1, 2];var yourArray = [4, 5, 6];
這兩個數組將共用一個相同的prototype對象,下面的代碼將返回true 複製代碼 代碼如下:Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我們在兩個數組中調用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 == Object.getPrototypeOf(myArray)
// also true
Array.prototype == Object.getPrototypeOf(yourArray);
我們用新學的知識重畫之前的圖片:
圖 5
現在我們要建立一個array對象。其中一種方法就是: 複製代碼 代碼如下:// create a new, empty object
var o = {};
// inherit from the same prototype as an array object
o.__proto__ = Array.prototype;
// now we can invoke any of the array methods ...
o.push(3);
儘管上面的代碼看起來不錯,但問題是不是每一個JS的環境都支援對象的_proto_屬性。幸運的是,JS內建一個標準的機制用來建立新對象同時設定對象的_proto_屬性,這就是“new”操作符。 複製代碼 代碼如下:var o = new Array();
o.push(3);
“new”操作符在JS中有三個重要的任務:首先,它建立一個新的Null 物件。接著,它設定這個新對象的_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的知識還是希望讀者能夠自己去探索和發現。