JavaScript中創(chuàng)建對象的7種模式

閱讀文本之前需要了解JavaScript的原型鏈閉包


在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)建方式戈锻。新特性“寄生”在舊的對象上歼跟。
寄生模式封裝了以下步驟:

  1. 使用已有的對象創(chuàng)建方法,創(chuàng)建新的實例對象
  2. 將這個對象增強(qiáng)(給它增加屬性)
  3. 返回這個對象
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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末非迹,一起剝皮案震驚了整個濱河市环鲤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌憎兽,老刑警劉巖冷离,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吵冒,死亡現(xiàn)場離奇詭異,居然都是意外死亡西剥,警方通過查閱死者的電腦和手機(jī)痹栖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞭空,“玉大人揪阿,你說我怎么就攤上這事∨匚罚” “怎么了南捂?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長旧找。 經(jīng)常有香客問我溺健,道長,這世上最難降的妖魔是什么钮蛛? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任矿瘦,我火速辦了婚禮,結(jié)果婚禮上愿卒,老公的妹妹穿的比我還像新娘缚去。我一直安慰自己,他們只是感情好琼开,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布易结。 她就那樣靜靜地躺著,像睡著了一般柜候。 火紅的嫁衣襯著肌膚如雪搞动。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天渣刷,我揣著相機(jī)與錄音鹦肿,去河邊找鬼。 笑死辅柴,一個胖子當(dāng)著我的面吹牛箩溃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碌嘀,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涣旨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了股冗?” 一聲冷哼從身側(cè)響起霹陡,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后烹棉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒霹,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年浆洗,在試婚紗的時候發(fā)現(xiàn)自己被綠了催束。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡辅髓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出少梁,到底是詐尸還是另有隱情洛口,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布凯沪,位于F島的核電站第焰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏妨马。R本人自食惡果不足惜挺举,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烘跺。 院中可真熱鬧湘纵,春花似錦、人聲如沸滤淳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脖咐。三九已至铺敌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屁擅,已是汗流浹背偿凭。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留派歌,地道東北人弯囊。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像胶果,于是被迫代替她去往敵國和親常挚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容