在js的編程中我們有時候要創(chuàng)建一批模板相同的變量心例。比如:
var person1 = {
name : 'p1',
age : 21
};
var person2 = {
name : 'p2',
age : 22
};
var person3 = {
name : 'p3',
age : 23
};
// ...
如果每一個對象都像這樣通過對象字面量(Object literals)直接定義,就要寫太多的重復(fù)代碼了瘪板。這些對象明明是結(jié)構(gòu)相同的驴娃,為什么不提取出相同的部分進(jìn)行代碼重用呢花竞?
因此煌恢,人們在實踐的過程中骇陈,就總結(jié)出了以下幾種創(chuàng)建對象的模式。
1. 工廠模式
工廠模式使用一個函數(shù)來封裝創(chuàng)建對象瑰抵、屬性賦值你雌、組裝對象,這個函數(shù)叫做工廠函數(shù)二汛。使用的時候只需要將對象的信息作為參數(shù)傳遞給工廠函數(shù)婿崭,工廠函數(shù)就會幫我們“加工”出需要的對象了。
function createPerson(name, age) {
var o = {};
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson("p1", 21);
var person2 = createPerson("p2", 22);
var person3 = createPerson("p3", 23);
工廠模式的缺陷
- 低效率肴颊,每次調(diào)用工廠函數(shù)都重復(fù)地定義了相同的函數(shù)(上面例子的sayName)氓栈。這些函數(shù)的作用完全相同,卻各自占用了內(nèi)存空間和cpu資源婿着。
- 對象識別困難授瘦。實例對象與它的工廠函數(shù)沒有關(guān)聯(lián)。給出一個對象竟宋,很難知道它是通過哪個模板構(gòu)造出來的(即對象的“類型”)提完。
2. 構(gòu)造函數(shù)模式
構(gòu)造函數(shù)模式的特點是:模板的所有屬性都通過構(gòu)造函數(shù)來定義。
js在最初設(shè)計的時候丘侠,為了讓其他面向?qū)ο笳Z言的程序員更好上手徒欣,給js加入了很多Java的特性,構(gòu)造函數(shù)和new關(guān)鍵字就是其中之一蜗字。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person("p1", 21);
var person2 = new Person("p2", 22);
var person3 = new Person("p3", 23);
像Java一樣使用js對象
構(gòu)造函數(shù)就像一個“類”打肝,它規(guī)定了對象的模板。而且挪捕,constructor指針和instanceof關(guān)鍵字的引入粗梭,讓對象與構(gòu)造函數(shù)關(guān)聯(lián)起來,可以進(jìn)行對象識別级零,就像在Java中識別一個對象是否為一個類的實例一樣:
console.log(person1.constructor === Person);
console.log(person1 instanceof Object);
console.log(person1 instanceof Person);
// 全部打印true
像使用Java一樣使用js断医,被很多專家批評是“不倫不類”的,而且會給人一種“js有類”的誤導(dǎo)妄讯。js本身的原型系統(tǒng)是非常強(qiáng)大而靈活的孩锡,掌握好以后酷宵,比Java的類系統(tǒng)更加好用亥贸。很多權(quán)威專家勸js程序員應(yīng)該慢慢擺脫使用構(gòu)造函數(shù)和new關(guān)鍵字。
構(gòu)造函數(shù)模式的缺陷
低效率浇垦。原因與工廠模式相同炕置。在上面這個例子中,我們每次執(zhí)行一次Person構(gòu)造函數(shù),都會定義一個sayName函數(shù)(函數(shù)在js中是對象)朴摊。所有的sayName函數(shù)作用都是一樣的默垄,重復(fù)的定義顯然是浪費cpu和內(nèi)存資源。我們可以這樣改進(jìn):
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
但是這樣做甚纲,又會有以下兩個問題:
- 在全局作用域定義的sayName僅僅只是為了給Person使用口锭,這讓全局作用域有點名不副實(全局作用域被濫用)。
- 如果Person需要定義很多方法介杆,那么就需要定義很多個全局函數(shù)鹃操,這會污染命名空間,而且封裝性很差春哨,代碼混亂荆隘。
基于以上原因,我們很少使用純的構(gòu)造函數(shù)模式赴背,文章的后面會展示如何將構(gòu)造函數(shù)模式與原型模式一起使用而避免這些問題椰拒。
3. 原型模式
原型模式的特點是:模板的所有屬性都定義在原型對象上,讓所有實例對象共享原型鏈上的屬性凰荚。每次創(chuàng)建對象只是創(chuàng)建一個[[prototype]]指向這個原型的空對象燃观。
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
friends: ["Shelby", "Court"],
sayName: function() {
alert(this.name);
},
constructor: Person
};
var person1 = new Person();
var person2 = new Person();
以上代碼雖然使用到了構(gòu)造函數(shù)和new關(guān)鍵字,但是我們并沒有在構(gòu)造函數(shù)內(nèi)添加屬性浇揩。使用它們僅僅是為了創(chuàng)建一個[[prototype]]指向這個模板的空對象仪壮。事實上,我們也可以不使用構(gòu)造函數(shù)來實現(xiàn)原型模式:
var personTemplate = {
name: "Nicholas",
age: 29,
friends: ["Shelby", "Court"],
sayName: function() {
alert(this.name);
}
};
var person1 = Object.create(personTemplate);
var person2 = Object.create(personTemplate);
原型函數(shù)模式的問題
使用原型模式不會出現(xiàn)構(gòu)造函數(shù)模式中重復(fù)定義函數(shù)的問題胳徽。但是會出現(xiàn)其他的問題:
- 如果不通過構(gòu)造函數(shù)來實現(xiàn)原型模式(比如通過Object.create來指定新對象的原型)积锅,會出現(xiàn)與工廠模式相同的對象識別問題。
- 在實例化的時候我們無法指定新對象的屬性值养盗。比如name和age的值始終都是默認(rèn)的"Nicholas"和29缚陷。程序員需要在創(chuàng)建實例以后自己將需要的name和age賦值給新對象。
- 原型鏈的改變會影響所有已經(jīng)構(gòu)造出的實例往核。這是一種靈活性箫爷,也是一種危險。如果我們不小心通過實例對象改變了原型鏈上的屬性聂儒,會影響所有的實例對象虎锚。比如:
person1.friedns.push('new friend');
person1.friedns和person2.friedns都會增加'new friend'
。
4. 組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)模式和原型模式定義“模板”的思想是完全不同的:
- 構(gòu)造函數(shù)模式的思路是每次實例化對象的時候都給新對象定義屬性衩婚。
- 原型模式的思路是所有實例對象共享同一條原型鏈窜护。
這兩者都有自己的缺陷,但是又不會出現(xiàn)對方的缺陷非春,因此很容易就想到柱徙,將這兩個模式一起使用缓屠,優(yōu)勢互補(bǔ)。
組合模式就是:通過構(gòu)造函數(shù)來定義那些需要屬于自己的屬性护侮,通過原型對象來定義那些需要共享的屬性(尤其是函數(shù))敌完。
function Person(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
};
var person1 = new Person("p1", 21, ['f1', 'f2']);
var person2 = new Person("p2", 22, ['f3', 'f4']);
這種創(chuàng)建對象的模式,是在js中被使用最多羊初、廣泛認(rèn)可的定義“對象模板”的方法滨溉。
使用原型鏈的時候,我們要作出合理的選擇:
- 哪些屬性是每個實例對象都擁有的长赞,需要在每次實例化的時候添加到實例對象上业踏。
- 哪些屬性是所有實例共享的,只需要定義在原型對象上涧卵。這可以減少資源的浪費勤家。
組合模式的缺陷
組合模式已經(jīng)相當(dāng)好用了。程序員對組合模式的主要抱怨是:構(gòu)造函數(shù)定義與原型定義的割裂柳恐。能不能將兩者放在同一個代碼塊中呢伐脖?這時候就需要下面的動態(tài)原型模式了。
5. 動態(tài)原型模式
動態(tài)原型模式可以看作是一種組合模式乐设,它將原型的配置放在了構(gòu)造函數(shù)中讼庇,使得“模板定義”的代碼集中在了一個代碼塊中。
function Person(name, age) {
this.name = name;
this.age = age;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
可以看出近尚,“類”的定義更加“一體化”了蠕啄。
6. 寄生模式
寄生模式利用已有的對象創(chuàng)建方式,封裝得到新的對象創(chuàng)建方式戈锻。新特性“寄生”在舊的對象上歼跟。
寄生模式封裝了以下步驟:
- 使用已有的對象創(chuàng)建方法,創(chuàng)建新的實例對象
- 將這個對象增強(qiáng)(給它增加屬性)
- 返回這個對象
function SpecialArray() {
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function() {
return this.join("|");
};
return values;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());
//"red|blue|green"
以上例子中格遭,使不使用new關(guān)鍵字哈街,結(jié)果都一樣。
寄生模式和工廠模式在本文的例子幾乎一樣拒迅,除了工廠函數(shù)前不能使用new關(guān)鍵字以外骚秦,兩者的區(qū)別主要在于設(shè)計思想上:
- 工廠模式是用來大量制造復(fù)雜對象的。要制造出這些復(fù)雜對象璧微,你可能需要在工廠函數(shù)中給對象添加屬性作箍、設(shè)置對象的原型鏈、拼接幾個組件(對象)前硫。
- 寄生模式用來制造已有對象的增強(qiáng)對象胞得。在寄生模式的函數(shù)中,一般都是給對象添加屬性开瞭。
寄生模式的缺陷
寄生模式的缺陷與工廠模式相同:低效率和對象識別懒震。
不管是否使用new關(guān)鍵字,寄生模式的函數(shù)都像一個工廠函數(shù)嗤详,與實例對象沒有關(guān)系个扰,出現(xiàn)對象識別問題。在上例中colors instanceof SpecialArray
的值為false葱色;colors.constructor
為Array递宅,而不是SpecialArray。
7. 穩(wěn)妥構(gòu)造函數(shù)模式
在一些特殊環(huán)境下苍狰,對象的使用者并不是對象的定義者办龄。定義者在定義對象的時候要防止他的對象被濫用,因此定義者就需要定義“穩(wěn)妥對象”來給使用者使用淋昭。
穩(wěn)妥對象(Durable Object)是這樣一種對象:
- 它的“信息”并不直接保存在對象屬性中俐填,以防被對象的使用者隨意訪問。
- 對象屬性上定義了一些工作方法翔忽。這些方法可以訪問到這些“信息”英融,以便完成工作。
- 在工作方法中絕不使用this指針歇式,而是通過閉包來訪問所需要的對象“信息”驶悟。
用來創(chuàng)建穩(wěn)妥對象的函數(shù)就叫穩(wěn)妥構(gòu)造函數(shù)。
function Car(make, model, year) { // 傳遞給Car的參數(shù)是私有變量
var o = new Object();
var condition = 'used'; // 私有變量
o.sayCar = function() { // 公有函數(shù)
console.log('I have a ' + condition + ' ' + year + ' ' + make + ' ' + model + '.');
};
return o;
}
var johnCar = Car('Ford', 'F150', '2011');
johnCar.sayCar();
// I have a used 2011 Ford F150.
johnCar對象是安全的材失,因為使用者只能夠調(diào)用它的sayCar方法痕鳍,而無法直接訪問它的make, model, year, condition信息。
這些“信息”可以理解為私有變量龙巨。
不一定要像上面這個例子一樣笼呆,通過工廠函數(shù)來實現(xiàn)穩(wěn)妥構(gòu)造函數(shù)模式,完全可以改成使用構(gòu)造函數(shù)來實現(xiàn)旨别,這樣還能解決對象識別的問題抄邀。可見這些對象創(chuàng)建模式并不是互斥的昼榛,只要掌握了它們的核心思想境肾,就可以各取所長:
function Car(make, model, year) { // 傳遞給Car的參數(shù)是私有變量
var condition = 'used'; // 私有變量
this.sayCar = function() { // 公有函數(shù)
console.log('I have a ' + condition + ' ' + year + ' ' + make + ' ' + model + '.');
};
}
var johnCar = new Car('Ford', 'F150', '2011');
johnCar.sayCar();
// I have a used 2011 Ford F150.
console.log(johnCar instanceof Car);
// true
重復(fù)定義sayCar函數(shù)是不能避免的,這是因為我們需要使用閉包胆屿,以便它能訪問make, model, year, condition這些變量奥喻。閉包的使用詳見徹底理解js閉包。
參考資料
《JavaScript高級程序設(shè)計》6.2