先介紹目前在ECMAScript中使用最廣泛,認同度最高的預設模式。
1.組合使用建構函式及原型
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"];}Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); }}var person1 = new Person('Nocholas',29,'Software Engineer');alert(person1.friends); //"Shelby,Count,Van"person1.sayName(); //"Nocholas"
其中執行個體屬性都是在建構函式中定義的,而由所有執行個體共用的屬性 constructor 和方法 sayName() 則是在原型中定義的。
constructor屬性始終指向建立當前對象的建構函式,請牢記此處。constructor屬性
2.建構函式模式
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName : function(){ alert(this.name); }}var person1 = new Person('Nocholas',29,'Software Engineer');var person2 = new Person('Greg',27,'Doctor');person1.sayName(); //"Nocholas"person1.sayName(); //"Greg"
按照慣例建構函式始終都應該以一個大寫字母開頭
特點:這種方法沒有顯式地建立對象;直接將屬性和方法賦給了 this 對象;沒有傳回值。
要建立Person的新執行個體時,必須使用 new 操作符。以這種方式調用建構函式實際上會經曆以下4個步驟:
(1) 建立一個新對象
(2) 將建構函式的範圍賦給對象(因此 this 就指向了這個新對象)
(3) 執行建構函式中的代碼(為這個新對象添加屬性)
(4) 返回新對象
我們來檢測一下物件類型
alert( person1 instanceof Object); //truealert( person1 instanceof Person); //truealert( person2 instanceof Object); //truealert( person2 instanceof Person); //true
這個例子中,person1 和 person2 都是Person的執行個體,同時所有對象均繼承自Object。
建構函式模式的缺點:
使用建構函式的主要問題就是每個方法都要在每個執行個體上重新建立一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName 的方法,但那兩個方法不是同一個 Function 的執行個體。 ECMAScript中的函數是對象,因此每定義一個函數就是執行個體化了一個對象,從邏輯角度講,此時的建構函式也可以這樣定義:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function(){ alert(this.name); }}
從這個角度看建構函式更容易明白每個Person 執行個體都包含一個不同的 Function 執行個體的本質,如前所述這兩個函數是不相等的,
alert(person1.sayName() == person2.sayName()) //false
我們沒有理由對實現同一功能的方法多次建立,特別是在方法數量較多的情況,即便是可以通過下面的方法來避免多次建立:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName;}function sayName(){ alert(this.name);}
我們建立全域函數 sayName,將建構函式內部的屬性設定成等於全域的 sayName 函數;由於sayName 包含的是一個指向函數的指標,person1 和 person2 對象就共用了全域範圍中的函數,這樣確實解決了兩個函數共做一件事的問題,可是這樣在全域範圍中的函數 sayName 只是為了Person 執行個體化的對象調用,讓全域範圍有點名不副實。
而更讓人無法接受的是要定義很多方法的時候,就要定義很多函數,於是我們自訂的類就變得絲毫沒有封裝性可言了。還好這些問題可以通過原型模式來解決。
3.原型模式
簡單理解:我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個對象,它的用途就是可以讓所有執行個體共用它包含的的屬性和方法。 換句話說,不必在建構函式中定義對象資訊,而是可以將這些資訊直接添加到原型對象中,如下所示:
function Person(){}Person.prototype.name = "Nicholas";proson.prototype.age = 29;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 屬性中,建構函式變成了空函數,即便如此也仍可以通過調用建構函式來建立一個新對象,而且新對象還會具有相同的實行和方法,新對象中的屬性和方法是由所有執行個體共用的。
下面我們來理解原型模式的工作原理,有點抽象,不過卻是js物件導向編程的最核心部分,理解他很重要,多看幾遍就是:
理解原型(prototype)
每一個JavaScript對象(null除外)都和另一個對象相關聯,“另一個”對象就是我們熟知的原型,每個對象都從原型繼承屬性。
無論什麼時候,只要建立了一個新函數,就會根據一組特定的規則為該函數建立一個prototype屬性,我們可以為 prototype 添加屬性和方法。預設情況下,prototype 屬性都會自動獲得一個 constructor 屬性,constructor屬性始終指向建立當前對象的建構函式,預設情況下指向函數自己,我們不用深究constructor。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"];}Person.prototype.sayName =function(){ alert(this.name);}console.log(Person.prototype.constructor === Person); //true
另外每個對象都會在其內部初始化一個屬性,就是__proto__,__proto__指向當前對象父物件的pertotype 當我們訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那麼他就會去__proto__裡找這個屬性,這個__proto__又會有自己的__proto__,於是就這樣一直找下去,也就是我們平時所說的原型鏈的概念。
function Person( name, age, job ){ this.namee = name; this.age = age; this.job = job;}Person.prototype.sayName = function(){ alert(this.name);}console.log(Person.__proto__ === Function.prototype); //true from Function;console.log("******************");console.log(Function.prototype.__proto__ === Object.prototype);console.log(Object.prototype.__proto__ === null); //true
原型鏈的起點是Object.prototype Object.prototype中包含著toString()、valueOf() 等內建方法,這也是各種資料類型的同名方法,其實都是繼承於此。
看下面,注意區分 prototype 和 __proto__ ,通俗的來理解:
一個普通的函數 function Person(){} 同時擁有 prototype 和 __proto__。 Person.prototype 包含著Person擁有的一切以後要傳給兒子的屬性和方法 ,一開始只包含一個constructor屬性 可以自由增加 Person.prototype.familyName = "陳";Person.prototype.skill = “泡妞”;
Person.__proto__ 則指向 Person的老爸的原型 Function.prototype,顯然預設也只包含一個constructor屬性 如果曾經發生過 Function.prototype.car = "勞斯萊斯",老爸有輛勞斯萊斯的車,那麼 console.log(Person.car) //勞斯萊斯Person也繼承了。
執行個體化的對象 person1 = new Person(); 是沒有prototype的 console.log(person.prototype); //undefined。其他的物件類型也一樣。
var arr = new Array();var fun = new Function();var obj= new Object();console.log(arr.prototype) //undefined console.log(fun .prototype) //undefined console.log(obj.prototype) //undefined
更簡單的原型文法
前面的例子中沒添加一個屬性和方法就要敲一遍 Person.prototype。為減少不需要的輸入,也從視覺上更好地封裝原型的功能,常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下
function Person(){}Person.prototype = {
constructor : Person, name : "Nicholas", age : 29, job : "Software Engineer", sayName : function(){ alert(this.name); } }
原型對象的問題:
首先,它省略了為建構函式傳遞初始化參數這一環節,結果所有實力在預設情況下都取得相同的屬性值,這會在某種程度上帶來一些不便,但這還不是原型的最大問題,原型的最大問題是由其共用的本性所導致的。
原型中的所有屬性是被很多執行個體共用的,這種共用對於函數非常合適。對於那些包含基本值的屬性倒也說得過去(通過在執行個體上添加一個同名屬性,可以隱藏原型中的對應屬性),然而對於包含參考型別值得屬性來說,問題就比較突出了。
如果對於實值型別,參考型別不太清楚的同學,請參閱 Javascript傳值方式
function Person(){}Psrson.prototype = { constructor : Person, name : "Nicholas", age : "29", friends : ["Shelby","Court"], sayName : function(){ alert(this.name); }}var person1 = new Person();var Person2 = new persin();person1.friends.push("Van"); // 向friends屬性添加一個元素alert(person1.friends); // ["Shelby","Court","Van"]alert(person2.friends); // ["Shelby","Court","Van"]alert(person1.friends === person2.friends); // true
由於Person的friends屬性是一個數組,是參考型別(對象),我們修改了person1.friends 引用的數組,向數組中添加了一個字串。由於friends數組存在於Person.prototype 而非 person1 中,所以我們的修改會影響到到所有的執行個體,假如我們的初衷就是這樣在所有執行個體中共用一個數組,那麼這個結果倒也可以接受,可是執行個體一般都是要有屬於自己的全部屬性的,而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。
我們最常見的方式,就是在開篇中介紹的組合使用建構函式模式與原型模式,建構函式用於定義執行個體屬性,原型模式用於定義方法和共用屬性。結果,每個執行個體都會有自己的一份執行個體屬性的副本,但同時又共用著對方法的引用,最大限度地節省了記憶體。另外這種混成模式還支援向建構函式傳遞參數,可謂是集兩種模式之長。
註:本文知識點源自《javascript進階程式設計》,想要對javascript物件導向瞭解更多的園友,可以自行查閱。
如果感覺本文對您有所助益,勞駕您推薦下,在此謝過。