| 文章簡介:我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。如果按照字面量的意思來理解,那麼prototype就是通過調用建構函式而建立的那個對象執行個體的原型對象。使用原型對象 |
我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。如果按照字面量的意思來理解,那麼prototype就是通過調用建構函式而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。換句話說,不必在建構函式中定義對象執行個體的資訊,而是可以將資訊直接添加到原型對象中,如下面的例子所示:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.age = "29";Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);};var person1 = new Person();person1.sayName(); //"Nicholas"var person2 = new Person();person2.sayName(); //"Nicholas";alert(person1.sayName == person2.sayName); //true
在此,我們將sayName()方法和所有屬性直接添加了Person的prototype屬性中,建構函式變成了空函數。即使如此,也仍然可以通過調用建構函式來建立新對象,而且新對象還會具有相同的屬性和方法。但與建構函式模式不同的是,新對象的這些屬性和方
法是由所有執行個體共用的。換句話說,person1和person2訪問的都是同一組屬性和同一個sayName()函數。要理解原型模式的工作原理,必須理解ECMAScript中原型對象的性質。
理解原型對象
無論什麼時候,只要建立了一個新函數,就會根據一組特定的規則為該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在預設情況下,所有原型對象都會獲得一個constructor(建構函式)屬性,這個屬性包含一個指向prototype屬性所在函數的指標。就拿前面的例子來說,Person.prototype.constructor指向Person。而通過這個建構函式,我們還可以繼續為原型對象添加其它屬性和方法。
建立了自訂的建構函式之後,其原型對象預設只會取得constructor屬性;至於其它方法,則都是從Object繼承而來的。當調用建構函式建立一個新執行個體後,該執行個體的內部將包含一個指標(內部屬性),指向建構函式的原型對象。在很多實現中,這個內部屬性的名字是_proto_,而且通過指令碼可以訪問到(在firefox、Safari、Chrome和Flash的ActionScript中,都可以通過指令碼訪問_proto_);而在其它實現中,這個屬性對指令碼則是完全不可見的。不過,要明確的真正重要的一點,就是這個串連存在與執行個體與建構函式的原型對象之間,而不是存在於執行個體於建構函式之間。
雖然在某些實現中無法訪問到內部的_proto_屬性,但在所有實現中都可以通過isPrototypeOf()方法來確定對象之間是否存在這種關係。從本質上來講,如果對象的_proto_指向調用isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就返回true,如下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true;alert(Person.prototype.isPrototypeOf(person2)); //true;
這裡,我們用原型對象的isPrototypeOf()方法測試了person1和person2。因為它們內部都有一個指向Person.prototype的指標,因此都返回了true。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從對象執行個體本身開始。如果在執行個體中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜尋指標指向的原型對象,在原型對象中尋找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值。也就是說,在我們調用person1.sayName()的時候,會先後執行兩次搜尋。首先,解析器會問:“執行個體person1有sayName屬性嗎?”答:“沒有。”然後,它繼續搜尋,再問:“person1的原型有sayName屬性嗎?”答:“有。”於是,它就讀取那個儲存在原型對象中的函數。當我們調用person2.sayName()時,將會重現相同的的搜尋過程,得到相同的結果。而這正是多個對象共用原型所儲存的屬性和方法的基本原理。
前面提到過,原型最初值包含constructor屬性,而該屬性也是共用的,因此可以通過對象執行個體訪問。
雖然可以通過對象執行個體訪問儲存在原型中的值,但卻不能通過對象執行個體重寫原型中的值。如果我們在執行個體中添加了一個屬性,而該屬性與執行個體原型中的一個屬性同名,那我們就在執行個體中建立該屬性,該屬性將會屏蔽原型中的那個屬性。來看下面的例子:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.ae = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);}var person1 = new Person();var person2 = new Person();person1.name = "Greg";alert(person1.name); //"Greg" ——來自執行個體alert(person2.name); //"Nicholas" ——來自原型
在這個例子中,person1的name被一個新值給屏蔽了。但無論訪問person1.name還是訪問person2.name都能正常地放回值,即分別是“Greg”(來自對象執行個體)和“Nicholas”(來自原型)。當在alert()中訪問person1.name時,需要讀取它的值,因此就會在這個執行個體上搜尋一個名為Name的屬性。這個屬性確實存在,於是就返回它的值而不必再搜尋原型了。當以同樣的方式訪問person2.name時,並沒有在執行個體上發現該屬性,因此就會繼續搜尋原型,結果在那裡找到了name屬性。
當為對象執行個體添加了一個屬性時,這個屬性就會屏蔽原型對象匯總儲存的同名屬性;換句話說,添加這個屬性只會組織我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性值設定為null,也只會在執行個體中設定這個屬性。而不會回複其指向原型的串連。不會,使用delete操作符則可以完全刪除執行個體屬性,從而讓我們能夠重新訪問原型中的屬性,如下所示:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.age = 20;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);}var person1 = new Person();var person2 = new Person();person1.name = "Greg";alert(person1.name); //“Greg” ——來自執行個體alert(person2.name); //"Nicholas" ——來自原型delete person1.name;alert(person1.name); //"Nicholas" ——來自原型
在這個修改後的例子中,我們使用delete操作符刪除了person1.name,之前它儲存的“Greg”值屏蔽了同名的原型屬性。把它刪除以後,就恢複了原型中name屬性的串連。因此,接下來再調用person1.name時,返回的就是原型中的name屬性的值了。
使用hasOwnProperty()方法可以檢測一個屬性是存在與執行個體中,還是存在於原型中。這個方法(不要忘了它是從Object繼承來的)只在給定屬性存在於對象執行個體中時,才會返回true。來看下面這個例子:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.jog = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);};var person1 = new Person();var person2 = new Person();alert(person1.hasOwnProperty("name")); //falseperson1.name = "Greg";alert(person1.name); //"Greg" ——來自執行個體alert(person1.hasOwnProperty("name")); //truealert(person2.name); //"Nicholas" ——來自原型alert(person2.hasOwnProperty("name")); //falsedelete person1.name;alert(person1.name); //"Nicholas" ——來自原型alert(person1.hasOwnProperty("name")); //flase
通過使用hasOwnProperty()方法,什麼時候訪問的是執行個體屬性,什麼時候訪問的是原型屬性就一清二楚了。調用person1.hasOwnProperty(“name”)時,只有當person1重寫name屬性後才會返回true,因為只有這時候name才是一個執行個體屬性,而非原型屬性。
原型與in操作符
有兩種方式使用in操作符:單獨使用和在for-in迴圈中使用。在單獨使用時,in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在於執行個體還是原型中。看一看下面的例子:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.age = "29";Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);};var person1 = new Person();var person2 = new Person();alert(person1.hasOwnProperty("name")); //falsealert("name" in person1); //trueperson1.name = "Greg";alert(person1.name); //"Greg" ——來自執行個體alert(person1.hasOwnProperty("name")); //true;alert("name" in person1); //truealert(person2.name); //"Nicholas"——來自原型alert(person2.hasOwnProperty("name")); //falsealert("name" in person1); //truedelete person1.name;alert(person1.name); //"Nicholas" ——來自原型alert(person1.hasOwnProperty("name")); //falsealert("name" in person1); //true;
在以上代碼執行的整個過程中,name屬性要麼直接在對象上訪問到,要麼是通過原型訪問到的。因此,調用“name” in Person1始終都返回true,無論該屬性存在與執行個體中還是存在與原型中。同時使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在於對象中,還是存在於原型中,如下所示:
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object);}
由於in操作符只要通過對象能夠訪問到屬性就就返回true,hasOwnProperty()只在屬性存在於執行個體中時才返回true,因此只要in操作符返回true而hasOwnProperty()返回false,就可以確定屬性是原型中的屬性。下面來看一看上面定義的函數hasPrototypeProperty()用法:
function Person() {}Person.prototype.name = "Nicholas";Person.prototype.age = "29";Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { alert(this.name);};var person = new Person();alert(hasPrototypeProperty(person, "name")); //trueperson.name = "Greg";alert(hasPrototypeProperty(person, "name")); //false
在這裡,name屬性先是存在於原型中,因此hasPrototypeProperty()返回true。當在執行個體中重寫name屬性後,該屬性就存在於執行個體中了,因此hasPrototypeProperty()返回false。
在使用for-in迴圈時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於執行個體中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即設定了[[DontEnum]]標記的屬性)的執行個體屬性也會在for-in迴圈中返回,因為規定,所有開發人員定義的屬性都是可枚舉的——只有IE除外。
IE的JScript實現中存在一個bug,即屏蔽了不可枚舉屬性的執行個體屬性不會出現在for-in迴圈中。例如:
var o = { toString: function () { return "My Object"; }}for (var prop in o) { if (prop == "toString") { alert("Found toString"); //在IE中不會顯示 }}
當以上代碼運行時,應該會顯示一個警告框,表明找到了toString()方法。這裡的對象o定義了一個名為toString()的方法,該方法屏蔽了原型中(不可枚舉)的toString()方法。在IE中,由於其實現認為原型的toString()方法被打上了[[DontEnum]]標記就應該跳過該屬性,結果我們就不會看到警告框。該bug會影響預設不可枚舉的所有屬性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。有的瀏覽器也為constructor和prototype屬性打上了[[DontEnum]]標記,但這並不是所有瀏覽器共同的做法。
更簡單的原型文法
讀者大概注意到了,前面例子中每添加一個屬性和方法就要敲一遍Person.prototype。為了減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下面的例子所示:
function Person() {}Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name); }};
在上面的代碼中,我們將Person.prototype設定為等於一個以對象字面量形式建立的新對象。最終結果相同,但有一個例外:constructor屬性不再指向Person了。前面曾經介紹過,每建立一個函數,就會同時建立它的prototype對象,這個對象也會自動獲得constructor屬性。而我們在這裡使用的文法,本質上完全重寫了預設的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性(指向Object建構函式),不再指向Person函數。此時,儘管instanceof操作符還能返回正確的結果,但通過constructor已經無法確定對象的類型了,如下所示:
function Person() {}Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name); }};var person = new Person();alert(person instanceof Object); //truealert(person instanceof Person); //truealert(person.constructor == Person); //falsealert(person.constructor == Object); //true
在此,用instanceof操作符測試Object和Person仍然返回true,但constructor屬性則等於Object而不等於Person了。如果constructor的值真的很重要,可以像下面這樣特意將它設定回適當的值:
function Person() {}Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name); }};var person = new Person();
以上代碼特意包含了一個constructor屬性,並將它的值設定為Person,從而確保了通過該屬效能夠訪問到適當的值。
alert(person instanceof Object); //truealert(person instanceof Person); //truealert(person.constructor == Person); //truealert(person.constructor == Object); //false
原型的動態性
由於在原型中尋找值的過程是一次搜尋,因此我們對原型對象所做的任何修改都能夠立即從執行個體上反映出來——即使是西安建立了執行個體後修改原型也照樣如此。請看下面的例子:
var person = new Person();Person.prototype.sayHi = function () { alert("hi");};person.sayHi(); //"hi" (沒有問題!)
以上代碼先建立了Person的一個執行個體,並將其儲存在person中。然後,下一條語句在Person.prototype中添加了一個方法sayHi()。即使person執行個體是在添加新方法之前建立的,但它仍然可以訪問這個新方法。其原因可以歸結為執行個體與原型之間的鬆散串連關係。當我們調用Person.sayHi()時,首先會在執行個體中搜尋名為sayHi的屬性,在沒找到的情況下,會繼續搜尋原型。因為執行個體與原型之間的串連只不過是一個指標,而非一個副本,因此就可以在原型中找到新的sayHi屬性並返回儲存在那裡的函數。
儘管可以隨時為原型添加屬性和方法,並且修改能夠立即在所有對象實力中反映出來,但如果是重寫整個原型對象,那麼情況就不一樣了。我們知道,調用建構函式時回味執行個體添加一個指向最初原型的_proto_指標,而把原型修改為另外一個對象就等於切斷了建構函式與最初原型之間的聯絡。請記住:執行個體中的指標近指向原型,而不指向建構函式。看下面的例子:
function Person() {}var person = new Person();Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software engineer", sayName: function () { alert(this.name); }};person.sayName(); //error
在這個例子中,我們先建立了Person的一個執行個體,然後又重寫了其原型對象。然後在調用person.sayName()時發生了錯誤,因為person指向的原型中不包含以改名字命名的屬性。
原生對象的原型
圓心模式的重要性不僅體現在建立自訂類型方面,就連所有原生的參考型別,都是採用這種模式建立的。所有原生參考型別(Object、Array、String、等等)都在其建構函式的原型上定義了方法。例如,在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法,如下所示:
alert(typeof Array.prototype.sort); //"function"alert(typeof String.prototype.substring); //"function"
通過原生對象的原則,不僅可以取得所有預設方法的引用,而且也可以定義新方法。可以向修改自訂對象的原型一樣修改原生對象的原型,因此尅隨時添加方法。下面的代碼就給基本封裝類型String添加了yield名為startsWith()的方法:
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0;};var msg = "Hello world!";alert(msg.startsWith("Hello")); //true
這裡定義的startsWith()方法會在傳入的文本位於yield字串開始時返回true。既然方法被添加給了String.pprototype,那麼當前環境中的所有字串就都可以調用它。由於msg是字串。而且後台會調用String基本保皇函數建立這個字串,因此通過msg就可以調用startsWith()方法。
儘管可以這樣做,但我們不推薦在產品化的程式中修改原生對象的原型。如果因某個實現中缺少某個方法,就在原生對象的原型中添加這個方法,那麼當在另一個支援該方法的實現中運行代碼時,就可能會導致命名衝突。而且,這樣做也可能意外地重寫原生方法。
原型對象的問題
原型模式也不是沒有缺點。首先,它省略了為建構函式傳遞初始化參數這一環節,結果所有執行個體在預設情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。原型模式的最大問題是由其共用的本性所導致的。
原型中所有屬性是被很多執行個體共用的,這種共用對於函數非常合適。對於那些包含基本值的屬性倒也說的過去,畢竟,通過在執行個體上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含參考型別的屬性來說,問題就比較突出了。來看下面的例子:
function Person() {}Person.prototype = { constructor: Person, name: "Nicholas", age: 2, job: "Software Engineer", friends: ["Shelby", "Court"], sayName: function () { alert(this.name); }};var person1 = new Person();var person2 = new Person();person1.friends.push("Van");alert(person1.friends); //"Shelby,Court,Van"alert(person2.friends); //"Shelby, Court, Van"alert(person1.friends == person2.friends); //ture
在此,Person.prototype對象有一個名為friends的屬性,該屬性包含一個字串數組。然後,建立了Person的兩個執行個體。接著,修改了person1.friends引用數組,向數組中添加了一個字串。由於firends數組存在與Person.prototype而非person1中,所以剛剛提到的修改也會通過person2.friends反映出來。假如我們的初衷就是像這樣在所有執行個體中共用一個數組,那麼對這個結果我沒有話可說。可是,執行個體一般都是要有屬於自己的全部屬性的。而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。