注:本文來自微博網友@三月沙 的翻譯投稿。
Javascript原型總會給人產生一些困惑,無論是經驗豐富的專家,還是作者自己也時常表現出對這個概念某些有限的理解,我認為這樣的困惑在我們一開始接觸原型時就已經產生了,它們常常和new、constructor相關,特別是函式(function)的原型(prototype)屬性(property)。事實上,原型是一種非常簡單的概念。為了更好的理解它,我們應該首先記住這個原則,那就是忘記我們已經學到的關於構造原型(construtor prototypes)的認識。
什麼是原型?
原型是一個物件,其他物件可以通過它實現屬性繼承。
任何一個物件都可以成為原型麼?
是
哪些物件有原型
所有的物件在預設的情況下都有一個原型,因為原型本身也是物件,所以每個原型自身又有一個原型(只有一種例外,預設的物件原型在原型鏈的頂端。更多關於原型鏈的將在後面介紹)
好吧,再繞回來,那什麼又是物件呢?
在javascript中,一個物件就是任何無序鍵值對的集合,如果它不是一個主資料型別(undefined,null,boolean,number,or string),那它就是一個物件
你說每個物件都有一個原型,可是我當我寫成({}).prototype 我得到了一個null,你說的不對吧?
忘記你已經學到的關於原型屬性的一切,它可能就是你對原型困惑的根源所在。一個物件的真正原型是被物件內部的[[Prototype]]屬性(property)所持有。ECMA引入了標準物件原型訪問器Object.getPrototype(object),到目前為止只有Firefox和chrome實現了此訪問器。除了IE,其他的瀏覽器支援非標準的訪問器__proto__,如果這兩者都不起作用的,我們需要從物件的建構函式中找到的它原型屬性。下面的程式碼展示了獲取物件原型的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var a = {}; //Firefox 3.6 and Chrome 5 Object.getPrototypeOf(a); //[object Object] //Firefox 3.6, Chrome 5 and Safari 4 a.__proto__; //[object Object] //all browsers a.constructor.prototype; //[object Object] |
ok,一切都進行的很好,但是false明明是一個主資料型別,可是false.__proto__卻返回了一個值
當你試圖獲取一個主資料型別的原型時,它被強制轉化成了一個物件
1 2 3 |
//(works in IE too, but only by accident) false.__proto__ === Boolean(false).__proto__; //true |
我想在繼承中使用原型,那我該怎麼做?
如果僅僅只是因為一個例項而使用原型是沒有多大意義的,這和直接新增屬性到這個例項是一樣的,假如我們已經建立了一個例項物件 ,我們想要繼承一個已經存在的物件的功能比如說Array,我們可以像下面這樣做( 在支援__proto__ 的瀏覽器中)
1 2 3 4 |
//unusual case and does not work in IE var a = {}; a.__proto__ = Array.prototype; a.length; //0 |
———————————————————————————————————–
譯者注:上面這個例子中,首先建立了一個物件a,然後通過a的原型來達到繼承Array 這個已經存在的物件的功能
———————————————————————————————————–
原型真正魅力體現在多個例項共用一個通用原型的時候。原型物件(注:也就是某個物件的原型所引用的物件)的屬性一旦定義,就可以被多個引用它的例項所繼承(注:即這些例項物件的原型所指向的就是這個原型物件),這種操作在效能和維護方面其意義是不言自明的
這也是建構函式的存在的原因麼?
是的。建構函式提供了一種方便的跨瀏覽器機制,這種機制允許在建立例項時為例項提供一個通用的原型
在你能夠提供一個例子之前,我需要知道constructor.prototype 屬性究竟是什麼?
首先,javascript並沒有在建構函式(constructor)和其他函式之間做區分,所以說每個函式都有一個原型屬性。反過來,如果不是函式,將不會有這樣一個屬性。請看下面的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//function will never be a constructor but it has a prototype property anyway Math.max.prototype; //[object Object] //function intended to be a constructor has a prototype too var A = function(name) { this.name = name; } A.prototype; //[object Object] //Math is not a function so no prototype property Math.prototype; //null |
現在我們可以下個定義了:函式A的原型屬性(prototype property )是一個物件,當這個函式被用作建構函式來建立例項時,該函式的原型屬性將被作為原型賦值給所有物件例項(注:即所有例項的原型引用的是函式的原型屬性)
———————————————————————————————————-
譯者注:以下的程式碼更詳細的說明這一切
var b = function(){ var one; }
var c = new b();
c.constructor==b //true
//b是一個函式,檢視b的原型如下
b.constructor.prototype // function (){}
b.__proto__ //function (){}
//b是一個函式,由於javascript沒有在建構函式constructor和函式function之間做區分,所以函式像constructor一樣,
//有一個原型屬性,這和函式的原型(b.__proto__ 或者b.construtor.prototype)是不一樣的
b.prototype //[object Object] 函式b的原型屬性
b.prototype==b.constructor.prototype //fasle
b.prototype==b.__proto__ //false
b.__proto__==b.constructor.prototype //true
//c是一個由b建立的物件例項,檢視c的原型如下
c.constructor.prototype //[object Object] 這是物件的原型
c.__proto__ //[object Object] 這是物件的原型
c.constructor.prototype==b.prototype; //true c的原型和b的原型屬性比較
//為函式b的原型屬性新增一個屬性max
的原型也會改變
———————————————————————————————————-
理解一個函式的原型屬性(function’s prototype property )其實和實際的原型(prototype)沒有關係對我們來說至關重要
1 2 3 4 5 6 7 8 9 10 11 |
//(example fails in IE) var A = function(name) { this.name = name; } A.prototype == A.__proto__; //false A.__proto__ == Function.prototype; //true - A's prototype is set to its constructor's prototype property |
給個例子撒
你可能曾經上百次的像這樣使用javascript,現在當你再次看到這樣的程式碼的時候,你或許會有不同的理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//Constructor. <em>this</em> is returned as new object and its internal [[prototype]] property will be set to the constructor's default prototype property var Circle = function(radius) { this.radius = radius; //next line is implicit, added for illustration only //this.__proto__ = Circle.prototype; } //augment Circle's default prototype property thereby augmenting the prototype of each generated instance Circle.prototype.area = function() { return Math.PI*this.radius*this.radius; } //create two instances of a circle and make each leverage the common prototype var a = new Circle(3), b = new Circle(4); a.area().toFixed(2); //28.27 b.area().toFixed(2); //50.27 |
棒極了。如果我更改了建構函式的原型,是否意味著已經存在的該建構函式的例項將獲得建構函式的最新版本?
不一定。如果修改的是原型屬性,那麼這樣的改變將會發生。因為在a實際被建立之後,a.__proto__是一個對A.prototype 的一個引用,。
1 2 3 4 5 6 7 |
var A = function(name) { this.name = name; } var a = new A('alpha'); a.name; //'alpha' A.prototype.x = 23; a.x; //23 |
——————————————————————————————————
譯者注:這個和上例中的一樣,例項物件a的原型(a.__proto__)是對函式A的原型屬性(A.prototype)的引用,所以如果修改的是A的原型屬性,
改變將影響由A建立的物件例項a 在下面的例子中,但是對函式A的原型進行了修改,但是並沒有反應到A所建立的例項a中
var A = function(name)
{
this.name = name;
}
var a = new A(‘alpha’);
a.name; //’alpha’
A.__proto__.max = 19880716;
a.max //undefined
——————————————————————————————————
但是如果我現在替換A的原型屬性為一個新的物件,例項物件的原型a.__proto__卻仍然引用著原來它被建立時A的原型屬性
1 2 3 4 5 6 7 |
var A = function(name) { this.name = name; } var a = new A('alpha'); a.name; //'alpha' A.prototype = {x:23}; a.x; //null |
——————————————————————————————————————
譯者注:即如果在例項被建立之後,改變了函式的原型屬性所指向的物件,也就是改變了建立例項時例項原型所指向的物件
但是這並不會影響已經建立的例項的原型。
——————————————————————————————————————-
一個預設的原型是什麼樣子的?
1 2 3 4 |
var A = function() {}; A.prototype.constructor == A; //true var a = new A(); a.constructor == A; //true (a's constructor property inherited from it's prototype) |
instance of 和原型有什麼關係
如果a的原型屬於A的原型鏈,表示式 a instance of A 值為true。這意味著 我們可以對instance of 耍個詭計讓它不在起作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var A = function() {} var a = new A(); a.__proto__ == A.prototype; //true - so instanceof A will return true a instanceof A; //true; //mess around with a's prototype a.__proto__ = Function.prototype; //a's prototype no longer in same prototype chain as A's prototype property a instanceof A; //false |
還能使用原型做些什麼呢?
記住我曾經所提到過的每個建構函式都有一個原型屬性,它用來為每一個它所建立的例項提供原型。這同樣也適用原生態的建構函式Function,String等,擴充套件這個屬性,我們可以達到擴充套件指定建構函式的所有例項
我曾經在之前的很多文章中使用過這個技巧來演示函式的擴充。在tracer utility 這篇文章中所有的string例項都實現了times這個方法,對字串本身進行指定數目的複製
1 2 3 4 5 6 7 |
String.prototype.times = function(count) { return count < 1 ? '' : new Array(count + 1).join(this); } "hello!".times(3); //"hello!hello!hello!"; "please...".times(6);//"please...please...please...please...please...please..." |
告訴我繼承是怎樣通過原型來工作的。什麼是原型鏈?
因為每個物件和原型都有一個原型(注:原型也是一個物件),物件的原型指向物件的父,而父的原型又指向父的父,我們把這種通過原型層層連線起來的關係撐為原型鏈。這條鏈的末端一般總是預設的物件原型。
1 2 3 4 5 6 7 |
a.__proto__ = b; b.__proto__ = c; c.__proto__ = {}; //default object {}.__proto__.__proto__; //null |
原型的繼承機制是發生在內部且是隱式的.當想要獲得一個物件a的屬性foo的值,javascript會在原型鏈中查詢foo的存在,如果找到則返回foo的值,否則undefined被返回。
賦值呢?
原型的繼承 is not a player 當屬性值被設定成a.foo=’bar’是直接給a的屬性foo設定了一個值bar。為了把一個屬性新增到原型中,你需要直接指定該原型。
以上就是這篇文章所要講述的。我個人感覺自己對原型概念的理解還是比較深刻的,但我的觀點並不能代表一切。如有紕漏和異議,敬請告知。
我在哪裡還能獲得關於原型更多的資訊?
我推薦這篇由Dmitry A. Soshnikov所著的文章,非常優秀。
英文原文:http://javascriptweblog.wordpress.com/2010/06/07/understanding-javascript-prototypes/