在js的編程中我們有時(shí)候要?jiǎng)?chuàng)建一批模板相同的變量。比如:
var person1 = {
name : 'p1',
age : 21
};
var person2 = {
name : 'p2',
age : 22
};
var person3 = {
name : 'p3',
age : 23
};
// ...
如果每一個(gè)對(duì)象都像這樣通過對(duì)象字面量(Object literals)直接定義,就要寫太多的重復(fù)代碼了。這些對(duì)象明明是結(jié)構(gòu)相同的,為什么不提取出相同的部分進(jìn)行代碼重用呢?
因此,人們?cè)趯?shí)踐的過程中,就總結(jié)出了以下幾種創(chuàng)建對(duì)象的模式。
1. 工廠模式
工廠模式使用一個(gè)函數(shù)來封裝創(chuàng)建對(duì)象、屬性賦值、組裝對(duì)象,這個(gè)函數(shù)叫做工廠函數(shù)。使用的時(shí)候只需要將對(duì)象的信息作為參數(shù)傳遞給工廠函數(shù),工廠函數(shù)就會(huì)幫我們“加工”出需要的對(duì)象了。
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資源。
- 對(duì)象識(shí)別困難。實(shí)例對(duì)象與它的工廠函數(shù)沒有關(guān)聯(lián)。給出一個(gè)對(duì)象,很難知道它是通過哪個(gè)模板構(gòu)造出來的(即對(duì)象的“類型”)。
2. 構(gòu)造函數(shù)模式
構(gòu)造函數(shù)模式的特點(diǎn)是:模板的所有屬性都通過構(gòu)造函數(shù)來定義。
js在最初設(shè)計(jì)的時(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對(duì)象
構(gòu)造函數(shù)就像一個(gè)“類”,它規(guī)定了對(duì)象的模板。而且,constructor指針和instanceof關(guān)鍵字的引入,讓對(duì)象與構(gòu)造函數(shù)關(guān)聯(lián)起來,可以進(jìn)行對(duì)象識(shí)別,就像在Java中識(shí)別一個(gè)對(duì)象是否為一個(gè)類的實(shí)例一樣:
console.log(person1.constructor === Person);
console.log(person1 instanceof Object);
console.log(person1 instanceof Person);
// 全部打印true
像使用Java一樣使用js,被很多專家批評(píng)是“不倫不類”的,而且會(huì)給人一種“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ù)模式的缺陷
低效率。原因與工廠模式相同。在上面這個(gè)例子中,我們每次執(zhí)行一次Person構(gòu)造函數(shù),都會(huì)定義一個(gè)sayName函數(shù)(函數(shù)在js中是對(duì)象)。所有的sayName函數(shù)作用都是一樣的,重復(fù)的定義顯然是浪費(fèi)cpu和內(nèi)存資源。我們可以這樣改進(jìn):
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
但是這樣做,又會(huì)有以下兩個(gè)問題:
- 在全局作用域定義的sayName僅僅只是為了給Person使用,這讓全局作用域有點(diǎn)名不副實(shí)(全局作用域被濫用)。
- 如果Person需要定義很多方法,那么就需要定義很多個(gè)全局函數(shù),這會(huì)污染命名空間,而且封裝性很差,代碼混亂。
基于以上原因,我們很少使用純的構(gòu)造函數(shù)模式,文章的后面會(huì)展示如何將構(gòu)造函數(shù)模式與原型模式一起使用而避免這些問題。
3. 原型模式
原型模式的特點(diǎn)是:模板的所有屬性都定義在原型對(duì)象上,讓所有實(shí)例對(duì)象共享原型鏈上的屬性。每次創(chuàng)建對(duì)象只是創(chuàng)建一個(gè)[[prototype]]指向這個(gè)原型的空對(duì)象。
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)建一個(gè)[[prototype]]指向這個(gè)模板的空對(duì)象。事實(shí)上,我們也可以不使用構(gòu)造函數(shù)來實(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ù)模式的問題
使用原型模式不會(huì)出現(xiàn)構(gòu)造函數(shù)模式中重復(fù)定義函數(shù)的問題。但是會(huì)出現(xiàn)其他的問題:
- 如果不通過構(gòu)造函數(shù)來實(shí)現(xiàn)原型模式(比如通過Object.create來指定新對(duì)象的原型),會(huì)出現(xiàn)與工廠模式相同的對(duì)象識(shí)別問題。
- 在實(shí)例化的時(shí)候我們無法指定新對(duì)象的屬性值。比如name和age的值始終都是默認(rèn)的"Nicholas"和29。程序員需要在創(chuàng)建實(shí)例以后自己將需要的name和age賦值給新對(duì)象。
- 原型鏈的改變會(huì)影響所有已經(jīng)構(gòu)造出的實(shí)例。這是一種靈活性,也是一種危險(xiǎn)。如果我們不小心通過實(shí)例對(duì)象改變了原型鏈上的屬性,會(huì)影響所有的實(shí)例對(duì)象。比如:
person1.friedns.push('new friend');
person1.friedns和person2.friedns都會(huì)增加'new friend'。
4. 組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)模式和原型模式定義“模板”的思想是完全不同的:
- 構(gòu)造函數(shù)模式的思路是每次實(shí)例化對(duì)象的時(shí)候都給新對(duì)象定義屬性。
- 原型模式的思路是所有實(shí)例對(duì)象共享同一條原型鏈。
這兩者都有自己的缺陷,但是又不會(huì)出現(xiàn)對(duì)方的缺陷,因此很容易就想到,將這兩個(gè)模式一起使用,優(yōu)勢(shì)互補(bǔ)。
組合模式就是:通過構(gòu)造函數(shù)來定義那些需要屬于自己的屬性,通過原型對(duì)象來定義那些需要共享的屬性(尤其是函數(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)建對(duì)象的模式,是在js中被使用最多、廣泛認(rèn)可的定義“對(duì)象模板”的方法。
使用原型鏈的時(shí)候,我們要作出合理的選擇:
- 哪些屬性是每個(gè)實(shí)例對(duì)象都擁有的,需要在每次實(shí)例化的時(shí)候添加到實(shí)例對(duì)象上。
- 哪些屬性是所有實(shí)例共享的,只需要定義在原型對(duì)象上。這可以減少資源的浪費(fèi)。
組合模式的缺陷
組合模式已經(jīng)相當(dāng)好用了。程序員對(duì)組合模式的主要抱怨是:構(gòu)造函數(shù)定義與原型定義的割裂。能不能將兩者放在同一個(gè)代碼塊中呢?這時(shí)候就需要下面的動(dòng)態(tài)原型模式了。
5. 動(dòng)態(tài)原型模式
動(dòng)態(tài)原型模式可以看作是一種組合模式,它將原型的配置放在了構(gòu)造函數(shù)中,使得“模板定義”的代碼集中在了一個(gè)代碼塊中。
function Person(name, age) {
this.name = name;
this.age = age;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
可以看出,“類”的定義更加“一體化”了。
6. 寄生模式
寄生模式利用已有的對(duì)象創(chuàng)建方式,封裝得到新的對(duì)象創(chuàng)建方式。新特性“寄生”在舊的對(duì)象上。
寄生模式封裝了以下步驟:
- 使用已有的對(duì)象創(chuàng)建方法,創(chuàng)建新的實(shí)例對(duì)象
- 將這個(gè)對(duì)象增強(qiáng)(給它增加屬性)
- 返回這個(gè)對(duì)象
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è)計(jì)思想上:
- 工廠模式是用來大量制造復(fù)雜對(duì)象的。要制造出這些復(fù)雜對(duì)象,你可能需要在工廠函數(shù)中給對(duì)象添加屬性、設(shè)置對(duì)象的原型鏈、拼接幾個(gè)組件(對(duì)象)。
- 寄生模式用來制造已有對(duì)象的增強(qiáng)對(duì)象。在寄生模式的函數(shù)中,一般都是給對(duì)象添加屬性。
寄生模式的缺陷
寄生模式的缺陷與工廠模式相同:低效率和對(duì)象識(shí)別。
不管是否使用new關(guān)鍵字,寄生模式的函數(shù)都像一個(gè)工廠函數(shù),與實(shí)例對(duì)象沒有關(guān)系,出現(xiàn)對(duì)象識(shí)別問題。在上例中colors instanceof SpecialArray的值為false;colors.constructor為Array,而不是SpecialArray。
7. 穩(wěn)妥構(gòu)造函數(shù)模式
在一些特殊環(huán)境下,對(duì)象的使用者并不是對(duì)象的定義者。定義者在定義對(duì)象的時(shí)候要防止他的對(duì)象被濫用,因此定義者就需要定義“穩(wěn)妥對(duì)象”來給使用者使用。
穩(wěn)妥對(duì)象(Durable Object)是這樣一種對(duì)象:
- 它的“信息”并不直接保存在對(duì)象屬性中,以防被對(duì)象的使用者隨意訪問。
- 對(duì)象屬性上定義了一些工作方法。這些方法可以訪問到這些“信息”,以便完成工作。
- 在工作方法中絕不使用this指針,而是通過閉包來訪問所需要的對(duì)象“信息”。
用來創(chuàng)建穩(wěn)妥對(duì)象的函數(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對(duì)象是安全的,因?yàn)槭褂谜咧荒軌蛘{(diào)用它的sayCar方法,而無法直接訪問它的make, model, year, condition信息。
這些“信息”可以理解為私有變量。
不一定要像上面這個(gè)例子一樣,通過工廠函數(shù)來實(shí)現(xiàn)穩(wěn)妥構(gòu)造函數(shù)模式,完全可以改成使用構(gòu)造函數(shù)來實(shí)現(xiàn),這樣還能解決對(duì)象識(shí)別的問題。可見這些對(duì)象創(chuàng)建模式并不是互斥的,只要掌握了它們的核心思想,就可以各取所長(zhǎ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ù)是不能避免的,這是因?yàn)槲覀冃枰褂?strong>閉包,以便它能訪問make, model, year, condition這些變量。閉包的使用詳見徹底理解js閉包。
參考資料
《JavaScript高級(jí)程序設(shè)計(jì)》6.2