如何優雅的去創建一個對象
在javascript中,創建一個對象有很多方法,但很多時候我們得根據我們的需求去選擇其中的一種來達到實現代碼簡單、優雅的表現,以下先來看下javascript中有哪些創建對象的方法,并指出優缺點再進行分類。
對象字面量形式-------------------------------快速,單純地創建某個對象
這個是最基本的了,就不再作描述
形式都是
var objectName = {
property1:value1,
property2:value2,
... ,
functionName1:function(){...};
functionName2:function(){...};
}
這種類型
優點
簡潔明了,適合快速創建某個對象
缺點
很明顯,創建一群類似的對象,就要這樣子重寫好多遍,明顯不合適,于是就有了以下各種模式的討論和比較了
工廠模式------------------------適用于小型項目或者未成型的項目
工廠模式主要考慮到在ECMAScript中無法創建類,因此用函數來封裝以特定接口創建對象的細節
如下:
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);
優點: 根據接受的參數來構建一個包含所有信息的對象,應用于多個相似對象的情況
缺點:沒有解決對象識別的問題(即怎樣知道一個對象的原型)
構造函數模式------------------適用于大型項目,需要定義各種特定類型
ECMAScript中,構造函數可以用來創建特定類型的對象,從而定義對象類型的屬性和方法
如:
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);
與工廠模式的區別:
- 沒有顯示地創建對象(交給了new操作符后臺,同時綁定原型,沒有配合new使用即this直接綁定在調用此函數的對象上,不算創建了對象)
- 直接將屬性和方法賦給this對象(因為this是動態綁定的)
- 沒有return語句(return語句交給了new操作符來指定
- 作為構造函數,應該以一個大寫字母開頭以作區別
然而有了構造函數還不夠,需要配合new操作符使用來創建對象
以下來深究下new一個對象,Javascript背后都發生了什么
首先構造函數我們還是使用上面Student函數
其實var student1 = new Student('LiAo',22,666);
我們先把它當作 var student1 = objectFactory(Student,'LiAo',22,666);
現在看看objectFactory到底是什么妖魔鬼怪,你才知道js在new的背后干了些什么,看以下代碼:
var objectFactory = function(){ var obj = new Object(); //創建一個空的對象; var Constructor = [].shift.call(arguments); //此處類數組對象arguments借用了數組的shift方法,取出第一項作為構造器 obj.__proto__ = Contructor.prototype; //給新創建的對象指向正確的圓原型對象 var ret = Constructor.apply(obj,arguments);//借用構造器給obj設置屬于它自己的屬性,此時的ret要根據構造函數是否返回值,不返回則為undefined return typeof ret ==='object' ? ret : obj; //若構造函數不返回對象,則ret = obj }; var studen1 = objectFactory(Student,'LiAo',22,666); console.log(student1.name) // LiAo
現在你大概懂了new操作符背后都干了些什么了吧,大概簡述如下:
- 創建一個新對象
- 新對象的原型指向構造器的原型
- 借用外部傳入的構造器為新對象設置唯一的屬性
- 返回新對象
優點: 可以將構造函數的實例標識為一種特定的類型,而不像工廠模式所有實例都是Object的直接實例
缺點: 每個方法都要在每個實例上重新創建一遍,不同實例上的同名函數是不相等的,有不同的作用域鏈和標識符解析
針對構造函數缺點的解決方案(非最佳實踐):
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
把函數定義轉移到構造函數外面來解決這個問題,但是這樣就催生了另一個問題:讓全局作用域有點名不副實,如果對象定義很多個函數,就要創建很多個全局函數,沒有封裝性可言, 這個問題就涉及到下面要討論的原型模式了.
原型模式------------------------------------------------很少單獨使用
從原型說起:
每個創建的函數都有一個prototype(原型)屬性,為一個指針指向一個對象,而這個對象用途為:包含可以由特定類型的所有實例共享的屬性和方法,構造函數創建了實例后,該實例的原型對象就指向了構造函數的原型屬性,從而在實例中搜索標識符的時候可以延伸到原型對象中搜索
換句話說,就是不必在構造函數中定義共享的屬性和方法,可以直接添加 到原型對象中.
注意 雖然可以通過對象實例來訪問保存在原型中的值,但不能通過對象實例重寫原型中的值,重寫只會在實例中創建屬性,從而屏蔽了原型中的屬性.(這里主要講創建對象的模式,就不在演示這些例子的代碼啦,大家有興趣可以回去翻翻高程三)
好的! 現在繼續講回原型
更簡單的原型語法:
function Person(){ } Person.prototype = { name:"LiAo", age:22, job:'Student', sayName:function(){ alert(this.name); } };
上面我們重寫了Person函數實例的原型對象,看起來語法看起來很簡單,但是默認的constructor屬性不再指向Person了,而是指向Object,但是記得現在不能用instanceof操作符來檢測實例的引用類型,因為都是顯示為true
student1 = new Person(); alert(student1 instanceof Object) // true; alert(student1 instanceof Person) // true
因為Person也是繼承Object的,此時可以用原型對象的constructor來檢測
如果constructor在你的開發需求中真的很重要,可以在重寫的對象中特意設一個constructor屬性指向Person
Person.prototype = { constructoe:Person, .... .... }
注意用這種方式來重設constructor屬性會導致他的[[Enumerable]]設置為true,從默認不可枚舉變成可枚舉屬性了,可以試試用es5中的Object.defineProperty()來定義constructor屬性
Object.defineProperty(Person.prototype,'constructor',{ enumerable:false, value:Person });
盡管可以重寫原型對象,但是要在實例化對象之前重寫對象,否則已經實例化的對象會斷開與新原型對象的聯系,如:
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
好了,感覺又跑偏了,關于原型的細節不是隨隨便便就能說完的,后續我再根據這個知識點做一個專門的總結吧,現在還是回到我們的主題,優雅地創建對象!
原型的優點:
原型模式很好地彌補了構造函數的缺點,并且簡化了初始化參數這一環節
隨之而來的缺點:
共享的本質導致:我們有時候并不想共享某些屬性,特別是引用類型,某個實例更改了引用類型同時也會修改掉其他實例的引用類型,因為在原型上定義屬性對于實例來說都是共享的.為了解決這個問題,于是又催生了下面的設計模式------組合模式
組合模式---------------------------------------使用最廣泛,認同度最高
組合模式本身不難理解,就是組合使用構造函數模式和原型模式
使用過程中注意區分好自身需求就好:
構造函數模式用于定義實例屬性.
原型模式用于定義方法和共的屬性.
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); } }
優點已經很明顯了,我就不再作闡述
動態原型模式-----------------------------------------必要時候才使用原型
動態原型模式倡導僅在必要時候才需要初始化原型
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); }; } }
動態原型模式有兩個特點
- 可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型
如果不進行檢查的話,每次初始化一個實例時,原型對象上的共享方法都會重新綁定一次,而進行檢查的話,可以回去看看上面的objectFactory函數,第一次初始化,每次經過檢查就無需在原型對象上再進行綁定. - if語句檢查的可以是初始化之后應該存在的任何屬性或方法 —— 不必用一大堆if語句檢查每個屬性和每個方法,只要檢查其中一個即可。
假如你需要定義很多共享屬性或者方法,那么只需要檢查一個就行,因為他們存在一致性,一者存在則所有者都存在.
動態原型模式的優點 - 把構造函數和原型模式結合在一個函數上,更有一體性
- 可以使用instanceof操作符確定實例的類型(因為沒有重寫原型對象)
動態原型模式的缺點 - 我覺得動態原型模式結合了以上每個模式的優點,但是降低了代碼的可讀性,如果習慣構造函數和原型對象分開寫的人可能覺得這并不那么優雅
- 另外還有就是不能采用重寫原型那樣簡潔的方法指定共享屬性或方法,如果共享屬性和方法特別多的話,就要寫好多Person.prototype.屬性(方法)這樣不優雅的代碼.
總之,見仁見智吧,每個人閱讀代碼的思維是不一樣的,找到自己順眼的一款就好了.
寄生構造函數模式-------------用于創建一個具有額外功能的特殊對象
我對寄生構造函數模式的記憶是想象成在構造函數里面再寄生一個構造函數,如下
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 ; }
你肯定會發現這個很像是典型的構造函數,內部又像工廠模式,你大可理解為構造函數和工廠模式的結合版.但是上面的例子用其他模式都有很好實現方法,不倡導如此使用,而是建議把寄生構造函數模式用于一些特殊情況,比如,創建一個具有額外方法的特殊數組,因為this無法修改,不能有this = new Array()這種情況,因此可以使用這個模式,返回一個對象替代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
需要注意的是:返回的對象與構造函數或者構造函數的原型屬性之間沒有關系.
至于為什么還有使用new操作符這個問題,我是這樣理解的:
參考上面的objectFactory,在這里,你會發現new不new都一樣的(因為Person函數中除了閉包外,沒有使用this,如果使用了this的話,那結果就會不一樣)。此時使用了new反而會在后臺多運行幾行沒必要的代碼,這里使用new的話我想估計是讓Person這個構造函數名副其實吧。
寄生構造函數模式的優點
- 可以基于引用類型創建一個特殊的對象
寄生構造函數模式的缺點
- 如上所示的注意點,因此不能用instanceof操作符來確定對象類型
- 適用范圍太窄,若能用其他模式實現創建對象,就不建議用次方法.
穩妥構造函數模式----------------------------適合在安全執行環境下使用
顧名思義,穩妥構造函數模式的目的是創建一個穩妥對象,穩妥對象是指沒有公共屬性,而且方法也不引用this對象,穩妥對象最適用于一些安全的環境或者防止數據被其他應用程序修改時使用。
穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:①新創建對象的實例方法不引用this。②不適用new操作符調用構造函數。
因此可以根據上面Person構造函數重寫如下:
function Person(name,age,job){ var o = new Object(); o.sayName = function(){ alert(name); }; return o ; var friend = Person('LiAo',22,'Student')
這也是高程三的栗子,但是真的要創建穩妥對象,首先是不能出現公共屬性,如果上面函數加了一句 o.name = name,那么外部仍然可以通過friend.name來得到name的值,所以這時候并不能稱為所謂的穩妥對象。或者說這個例子并不適合創建穩妥對象,因為name,age,job這些屬性應該要跟新對象o綁定起來,如果要構建一個名副其實的穩妥對象,那么可以這么寫
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'
這樣的話就算一個名副其實的穩妥對象了,但是何必呢,我覺得穩妥對象中的穩妥應該相對每個開發者而言,結合使用公共屬性和穩妥屬性來達到對有必要的數據進行保護的目的才是穩妥。
穩妥構造函數模式的優點
防止原始數據被其他應用環境進行修改,適合在某些安全執行環境下使用
穩妥構造函數模式的缺點
跟寄生構造函數一樣都是對象實例與構造函數之前沒有任何關系,因此不能用instanceof操作符來判斷類型。
最后的話
上面基本講完了ES6之前的所有創建對象的模式,分別有:
- 對象字面量形式
- 工廠模式
- 構造函數模式
- 原型模式
- 組合模式
- 動態原型模式
- 寄生構造函數模式
- 穩妥構造函數模式
大家可以根據自己需求來選擇不同的模式來創建對象,我覺得平時的創建對象的方法大多都是對象字面量和組合模式這兩種形式,其他模式看自己是否要區分類型或者考慮安全性、代碼優雅、是否要創建特殊對象等因素來進行選擇使用。
attention:上面的知識和例子大多參考紅寶書高程三,同時夾帶一些個人看法,如有錯誤或者不同看法,歡迎評論交(si)流(bi)哈,共同進步.