如何優(yōu)雅的去創(chuàng)建一個(gè)對(duì)象
在javascript中揍拆,創(chuàng)建一個(gè)對(duì)象有很多方法懂诗,但很多時(shí)候我們得根據(jù)我們的需求去選擇其中的一種來達(dá)到實(shí)現(xiàn)代碼簡單赢乓、優(yōu)雅的表現(xiàn),以下先來看下javascript中有哪些創(chuàng)建對(duì)象的方法贪薪,并指出優(yōu)缺點(diǎn)再進(jìn)行分類。
對(duì)象字面量形式-------------------------------快速眠副,單純地創(chuàng)建某個(gè)對(duì)象
這個(gè)是最基本的了画切,就不再作描述
形式都是
var objectName = {
property1:value1,
property2:value2,
... ,
functionName1:function(){...};
functionName2:function(){...};
}
這種類型
優(yōu)點(diǎn)
簡潔明了,適合快速創(chuàng)建某個(gè)對(duì)象
缺點(diǎn)
很明顯囱怕,創(chuàng)建一群類似的對(duì)象霍弹,就要這樣子重寫好多遍,明顯不合適娃弓,于是就有了以下各種模式的討論和比較了
工廠模式------------------------適用于小型項(xiàng)目或者未成型的項(xiàng)目
工廠模式主要考慮到在ECMAScript中無法創(chuàng)建類典格,因此用函數(shù)來封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)
如下:
function createStudent(name,age,grade){ var o = new Object(); o.name = name; o.age = age; o.grade = grade; o.sayName = function(){ alert(this.name); }; return o; } var student1 = createStudent('LiAo',22,666); var stundet2 = createStudent('Lan',22,999);
優(yōu)點(diǎn): 根據(jù)接受的參數(shù)來構(gòu)建一個(gè)包含所有信息的對(duì)象,應(yīng)用于多個(gè)相似對(duì)象的情況
缺點(diǎn):沒有解決對(duì)象識(shí)別的問題(即怎樣知道一個(gè)對(duì)象的原型)
構(gòu)造函數(shù)模式------------------適用于大型項(xiàng)目,需要定義各種特定類型
ECMAScript中台丛,構(gòu)造函數(shù)可以用來創(chuàng)建特定類型的對(duì)象耍缴,從而定義對(duì)象類型的屬性和方法
如:
function Student(name,age,grade){ this.name = name; this.age = age; this.grade = grade; this.sayName = function(){ alert(this.name); }; } var student1 = new Student('LiAo',22,666); var student2 = new Student('Lan',22,999);
與工廠模式的區(qū)別:
- 沒有顯示地創(chuàng)建對(duì)象(交給了new操作符后臺(tái),同時(shí)綁定原型挽霉,沒有配合new使用即this直接綁定在調(diào)用此函數(shù)的對(duì)象上防嗡,不算創(chuàng)建了對(duì)象)
- 直接將屬性和方法賦給this對(duì)象(因?yàn)閠his是動(dòng)態(tài)綁定的)
- 沒有return語句(return語句交給了new操作符來指定
- 作為構(gòu)造函數(shù),應(yīng)該以一個(gè)大寫字母開頭以作區(qū)別
然而有了構(gòu)造函數(shù)還不夠,需要配合new操作符使用來創(chuàng)建對(duì)象
以下來深究下new一個(gè)對(duì)象,Javascript背后都發(fā)生了什么
首先構(gòu)造函數(shù)我們還是使用上面Student函數(shù)
其實(shí)var student1 = new Student('LiAo',22,666);
我們先把它當(dāng)作 var student1 = objectFactory(Student,'LiAo',22,666);
現(xiàn)在看看objectFactory到底是什么妖魔鬼怪,你才知道js在new的背后干了些什么,看以下代碼:
var objectFactory = function(){ var obj = new Object(); //創(chuàng)建一個(gè)空的對(duì)象; var Constructor = [].shift.call(arguments); //此處類數(shù)組對(duì)象arguments借用了數(shù)組的shift方法,取出第一項(xiàng)作為構(gòu)造器 obj.__proto__ = Contructor.prototype; //給新創(chuàng)建的對(duì)象指向正確的圓原型對(duì)象 var ret = Constructor.apply(obj,arguments);//借用構(gòu)造器給obj設(shè)置屬于它自己的屬性,此時(shí)的ret要根據(jù)構(gòu)造函數(shù)是否返回值,不返回則為undefined return typeof ret ==='object' ? ret : obj; //若構(gòu)造函數(shù)不返回對(duì)象,則ret = obj }; var studen1 = objectFactory(Student,'LiAo',22,666); console.log(student1.name) // LiAo
現(xiàn)在你大概懂了new操作符背后都干了些什么了吧,大概簡述如下:
- 創(chuàng)建一個(gè)新對(duì)象
- 新對(duì)象的原型指向構(gòu)造器的原型
- 借用外部傳入的構(gòu)造器為新對(duì)象設(shè)置唯一的屬性
- 返回新對(duì)象
優(yōu)點(diǎn): 可以將構(gòu)造函數(shù)的實(shí)例標(biāo)識(shí)為一種特定的類型,而不像工廠模式所有實(shí)例都是Object的直接實(shí)例
缺點(diǎn): 每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍,不同實(shí)例上的同名函數(shù)是不相等的,有不同的作用域鏈和標(biāo)識(shí)符解析
針對(duì)構(gòu)造函數(shù)缺點(diǎn)的解決方案(非最佳實(shí)踐):
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外面來解決這個(gè)問題,但是這樣就催生了另一個(gè)問題:讓全局作用域有點(diǎn)名不副實(shí),如果對(duì)象定義很多個(gè)函數(shù),就要?jiǎng)?chuàng)建很多個(gè)全局函數(shù),沒有封裝性可言, 這個(gè)問題就涉及到下面要討論的原型模式了.
原型模式------------------------------------------------很少單獨(dú)使用
從原型說起:
每個(gè)創(chuàng)建的函數(shù)都有一個(gè)prototype(原型)屬性,為一個(gè)指針指向一個(gè)對(duì)象,而這個(gè)對(duì)象用途為:包含可以由特定類型的所有實(shí)例共享的屬性和方法,構(gòu)造函數(shù)創(chuàng)建了實(shí)例后,該實(shí)例的原型對(duì)象就指向了構(gòu)造函數(shù)的原型屬性,從而在實(shí)例中搜索標(biāo)識(shí)符的時(shí)候可以延伸到原型對(duì)象中搜索
換句話說,就是不必在構(gòu)造函數(shù)中定義共享的屬性和方法,可以直接添加 到原型對(duì)象中.
注意 雖然可以通過對(duì)象實(shí)例來訪問保存在原型中的值,但不能通過對(duì)象實(shí)例重寫原型中的值,重寫只會(huì)在實(shí)例中創(chuàng)建屬性,從而屏蔽了原型中的屬性.(這里主要講創(chuàng)建對(duì)象的模式,就不在演示這些例子的代碼啦,大家有興趣可以回去翻翻高程三)
好的! 現(xiàn)在繼續(xù)講回原型
更簡單的原型語法:
function Person(){ } Person.prototype = { name:"LiAo", age:22, job:'Student', sayName:function(){ alert(this.name); } };
上面我們重寫了Person函數(shù)實(shí)例的原型對(duì)象,看起來語法看起來很簡單,但是默認(rèn)的constructor屬性不再指向Person了,而是指向Object,但是記得現(xiàn)在不能用instanceof操作符來檢測實(shí)例的引用類型,因?yàn)槎际秋@示為true
student1 = new Person(); alert(student1 instanceof Object) // true; alert(student1 instanceof Person) // true
因?yàn)镻erson也是繼承Object的,此時(shí)可以用原型對(duì)象的constructor來檢測
如果constructor在你的開發(fā)需求中真的很重要,可以在重寫的對(duì)象中特意設(shè)一個(gè)constructor屬性指向Person
Person.prototype = { constructoe:Person, .... .... }
注意用這種方式來重設(shè)constructor屬性會(huì)導(dǎo)致他的[[Enumerable]]設(shè)置為true,從默認(rèn)不可枚舉變成可枚舉屬性了,可以試試用es5中的Object.defineProperty()來定義constructor屬性
Object.defineProperty(Person.prototype,'constructor',{ enumerable:false, value:Person });
盡管可以重寫原型對(duì)象,但是要在實(shí)例化對(duì)象之前重寫對(duì)象,否則已經(jīng)實(shí)例化的對(duì)象會(huì)斷開與新原型對(duì)象的聯(lián)系,如:
function Person(){}; var friend = new Person(); Person.prototype = { constructor:Person, name:'LiAo', age:22, job:'student', sayName:function(){ console.log(this.name); } } friend.sayName(); // error
好了,感覺又跑偏了,關(guān)于原型的細(xì)節(jié)不是隨隨便便就能說完的,后續(xù)我再根據(jù)這個(gè)知識(shí)點(diǎn)做一個(gè)專門的總結(jié)吧,現(xiàn)在還是回到我們的主題,優(yōu)雅地創(chuàng)建對(duì)象!
原型的優(yōu)點(diǎn):
原型模式很好地彌補(bǔ)了構(gòu)造函數(shù)的缺點(diǎn),并且簡化了初始化參數(shù)這一環(huán)節(jié)
隨之而來的缺點(diǎn):
共享的本質(zhì)導(dǎo)致:我們有時(shí)候并不想共享某些屬性,特別是引用類型,某個(gè)實(shí)例更改了引用類型同時(shí)也會(huì)修改掉其他實(shí)例的引用類型,因?yàn)樵谠蜕隙x屬性對(duì)于實(shí)例來說都是共享的.為了解決這個(gè)問題,于是又催生了下面的設(shè)計(jì)模式------組合模式
組合模式---------------------------------------使用最廣泛,認(rèn)同度最高
組合模式本身不難理解,就是組合使用構(gòu)造函數(shù)模式和原型模式
使用過程中注意區(qū)分好自身需求就好:
構(gòu)造函數(shù)模式用于定義實(shí)例屬性.
原型模式用于定義方法和共的屬性.
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ['Ye','Chen']; } Person.prototype = { constructor:Perosn, sayName:function(){ alert(this.name); } }
優(yōu)點(diǎn)已經(jīng)很明顯了,我就不再作闡述
動(dòng)態(tài)原型模式-----------------------------------------必要時(shí)候才使用原型
動(dòng)態(tài)原型模式倡導(dǎo)僅在必要時(shí)候才需要初始化原型
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Perosn.prototype.sayName = function(){ alert(this.name); }; } }
動(dòng)態(tài)原型模式有兩個(gè)特點(diǎn)
- 可以通過檢查某個(gè)應(yīng)該存在的方法是否有效,來決定是否需要初始化原型
如果不進(jìn)行檢查的話,每次初始化一個(gè)實(shí)例時(shí),原型對(duì)象上的共享方法都會(huì)重新綁定一次,而進(jìn)行檢查的話,可以回去看看上面的objectFactory函數(shù),第一次初始化,每次經(jīng)過檢查就無需在原型對(duì)象上再進(jìn)行綁定. - if語句檢查的可以是初始化之后應(yīng)該存在的任何屬性或方法 —— 不必用一大堆if語句檢查每個(gè)屬性和每個(gè)方法侠坎,只要檢查其中一個(gè)即可蚁趁。
假如你需要定義很多共享屬性或者方法,那么只需要檢查一個(gè)就行,因?yàn)樗麄兇嬖谝恢滦?一者存在則所有者都存在.
動(dòng)態(tài)原型模式的優(yōu)點(diǎn) - 把構(gòu)造函數(shù)和原型模式結(jié)合在一個(gè)函數(shù)上,更有一體性
- 可以使用instanceof操作符確定實(shí)例的類型(因?yàn)闆]有重寫原型對(duì)象)
動(dòng)態(tài)原型模式的缺點(diǎn) - 我覺得動(dòng)態(tài)原型模式結(jié)合了以上每個(gè)模式的優(yōu)點(diǎn),但是降低了代碼的可讀性,如果習(xí)慣構(gòu)造函數(shù)和原型對(duì)象分開寫的人可能覺得這并不那么優(yōu)雅
- 另外還有就是不能采用重寫原型那樣簡潔的方法指定共享屬性或方法,如果共享屬性和方法特別多的話,就要寫好多Person.prototype.屬性(方法)這樣不優(yōu)雅的代碼.
總之,見仁見智吧,每個(gè)人閱讀代碼的思維是不一樣的,找到自己順眼的一款就好了.
寄生構(gòu)造函數(shù)模式-------------用于創(chuàng)建一個(gè)具有額外功能的特殊對(duì)象
我對(duì)寄生構(gòu)造函數(shù)模式的記憶是想象成在構(gòu)造函數(shù)里面再寄生一個(gè)構(gòu)造函數(shù),如下
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o ; }
你肯定會(huì)發(fā)現(xiàn)這個(gè)很像是典型的構(gòu)造函數(shù),內(nèi)部又像工廠模式,你大可理解為構(gòu)造函數(shù)和工廠模式的結(jié)合版.但是上面的例子用其他模式都有很好實(shí)現(xiàn)方法,不倡導(dǎo)如此使用,而是建議把寄生構(gòu)造函數(shù)模式用于一些特殊情況,比如,創(chuàng)建一個(gè)具有額外方法的特殊數(shù)組,因?yàn)閠his無法修改,不能有this = new Array()這種情況,因此可以使用這個(gè)模式,返回一個(gè)對(duì)象替代this:
function SpecilArray(){ var values = new Array(); values.push.apply(values,arguments); values.toPipedString = function(){ return this.join('|'); }; return values; } var colors = new SpecialArray('red','blue','green'); alert(colors.toPipedString()); // red|blue|green
需要注意的是:返回的對(duì)象與構(gòu)造函數(shù)或者構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系.
至于為什么還有使用new操作符這個(gè)問題,我是這樣理解的:
參考上面的objectFactory,在這里,你會(huì)發(fā)現(xiàn)new不new都一樣的(因?yàn)镻erson函數(shù)中除了閉包外实胸,沒有使用this他嫡,如果使用了this的話,那結(jié)果就會(huì)不一樣)庐完。此時(shí)使用了new反而會(huì)在后臺(tái)多運(yùn)行幾行沒必要的代碼钢属,這里使用new的話我想估計(jì)是讓Person這個(gè)構(gòu)造函數(shù)名副其實(shí)吧。
寄生構(gòu)造函數(shù)模式的優(yōu)點(diǎn)
- 可以基于引用類型創(chuàng)建一個(gè)特殊的對(duì)象
寄生構(gòu)造函數(shù)模式的缺點(diǎn)
- 如上所示的注意點(diǎn),因此不能用instanceof操作符來確定對(duì)象類型
- 適用范圍太窄,若能用其他模式實(shí)現(xiàn)創(chuàng)建對(duì)象,就不建議用次方法.
穩(wěn)妥構(gòu)造函數(shù)模式----------------------------適合在安全執(zhí)行環(huán)境下使用
顧名思義门躯,穩(wěn)妥構(gòu)造函數(shù)模式的目的是創(chuàng)建一個(gè)穩(wěn)妥對(duì)象淆党,穩(wěn)妥對(duì)象是指沒有公共屬性,而且方法也不引用this對(duì)象生音,穩(wěn)妥對(duì)象最適用于一些安全的環(huán)境或者防止數(shù)據(jù)被其他應(yīng)用程序修改時(shí)使用宁否。
穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:①新創(chuàng)建對(duì)象的實(shí)例方法不引用this缀遍。②不適用new操作符調(diào)用構(gòu)造函數(shù)慕匠。
因此可以根據(jù)上面Person構(gòu)造函數(shù)重寫如下:
function Person(name,age,job){ var o = new Object(); o.sayName = function(){ alert(name); }; return o ; var friend = Person('LiAo',22,'Student')
這也是高程三的栗子,但是真的要?jiǎng)?chuàng)建穩(wěn)妥對(duì)象域醇,首先是不能出現(xiàn)公共屬性台谊,如果上面函數(shù)加了一句 o.name = name,那么外部仍然可以通過friend.name來得到name的值蓉媳,所以這時(shí)候并不能稱為所謂的穩(wěn)妥對(duì)象」Γ或者說這個(gè)例子并不適合創(chuàng)建穩(wěn)妥對(duì)象酪呻,因?yàn)閚ame,age,job這些屬性應(yīng)該要跟新對(duì)象o綁定起來,如果要構(gòu)建一個(gè)名副其實(shí)的穩(wěn)妥對(duì)象盐须,那么可以這么寫
function Person(name,age,job){ var o = new Object(); o.sayAge = function(){ alert(age); }; o.sayJob = function(){ alert(job) }; o.sayName = function(){ alert(name); }; return o ; var friend = Person('LiAo',22,'Student') o.sayAge(); //22 o.sayName(); 'LiAo' o.sayJob(); 'Student'
這樣的話就算一個(gè)名副其實(shí)的穩(wěn)妥對(duì)象了玩荠,但是何必呢,我覺得穩(wěn)妥對(duì)象中的穩(wěn)妥應(yīng)該相對(duì)每個(gè)開發(fā)者而言贼邓,結(jié)合使用公共屬性和穩(wěn)妥屬性來達(dá)到對(duì)有必要的數(shù)據(jù)進(jìn)行保護(hù)的目的才是穩(wěn)妥阶冈。
穩(wěn)妥構(gòu)造函數(shù)模式的優(yōu)點(diǎn)
防止原始數(shù)據(jù)被其他應(yīng)用環(huán)境進(jìn)行修改,適合在某些安全執(zhí)行環(huán)境下使用
穩(wěn)妥構(gòu)造函數(shù)模式的缺點(diǎn)
跟寄生構(gòu)造函數(shù)一樣都是對(duì)象實(shí)例與構(gòu)造函數(shù)之前沒有任何關(guān)系塑径,因此不能用instanceof操作符來判斷類型女坑。
最后的話
上面基本講完了ES6之前的所有創(chuàng)建對(duì)象的模式,分別有:
- 對(duì)象字面量形式
- 工廠模式
- 構(gòu)造函數(shù)模式
- 原型模式
- 組合模式
- 動(dòng)態(tài)原型模式
- 寄生構(gòu)造函數(shù)模式
- 穩(wěn)妥構(gòu)造函數(shù)模式
大家可以根據(jù)自己需求來選擇不同的模式來創(chuàng)建對(duì)象统舀,我覺得平時(shí)的創(chuàng)建對(duì)象的方法大多都是對(duì)象字面量和組合模式這兩種形式匆骗,其他模式看自己是否要區(qū)分類型或者考慮安全性、代碼優(yōu)雅誉简、是否要?jiǎng)?chuàng)建特殊對(duì)象等因素來進(jìn)行選擇使用碉就。
attention:上面的知識(shí)和例子大多參考紅寶書高程三,同時(shí)夾帶一些個(gè)人看法,如有錯(cuò)誤或者不同看法,歡迎評(píng)論交(si)流(bi)哈,共同進(jìn)步.