標籤:
【引言】在業務建模中,我們經常遇到這樣一種情況:“原型”對象負責實現業務的基本訴求(包括:有哪些屬性,有哪些函數以及它們之間的關係),以“原型”對象為基礎建立的“子物件”則實現一些個人化的業務特性,從而方便的實現業務擴充。最常見的搞法是:
1. 定義一個‘建構函式’,在其中實現屬性的初始化,例如:var Person = function( ){}; //函數體中可以進行一些變數的初始化。
2. 再設定該函數的prototype成員,例如:Person.prototype = { gotoSchool:function(){ console.log( ‘on foot‘ );} }; //該對象字面量中定義一些方法
3. 用new來建立一個新對象,例如:var student = new Person();
4. 個人化新對象的部分行為:student.gotoSchool = function(){ console.log( ‘by bus‘ ); } ;
>>根據new 和 原型鏈的特性,調用 student.gotoSchool(); 將會輸出 by bus,而不是 on foot。
5. 同理,用new來建立一個teacher的對象,然後再設定它的gotoSchool的成員。
var teacher = new Person(); teacher.gotoSchool = function(){ console.log( ‘by car‘ ); } ; teacher.gotoSchool() ; //將會輸出 by car
說明:本文中的代碼可以在Chrome瀏覽器的控制台中執行驗證。方法如下:按F12後單擊Console頁簽,開啟Chrome的控制台,可以看到console.log輸出的結果。
上面的方式能夠滿足我們的基本訴求,並且在之前的Web控制項自訂開發中,我們也是這麼做的。但是,如果業務模型比較複雜,那麼上面的這種方式的弊端也是明顯的:
沒有私人環境,所有的屬性都是公開的。
今天,我們就業務建模出發,看看如果藉助JavaScript的閉包特性,是否有更好的方式來優雅實現業務建模。
先看一個原型繼承的例子:
1 var BaseObject = (function(){ 2 var that = {}; 3 4 that.name = ‘Lily‘ ; 5 that.sayHello = function(){ 6 console.log( ‘Hello ‘ + this.getName() ); 7 }; 8 that.getName = function(){ 9 return this.name ;10 };11 12 return that ;13 })();14 15 //建立一個繼承的對象16 var tomObject = Object.create( BaseObject );17 tomObject.name = ‘Tom‘ ;18 19 //調用公開的方法20 tomObject.sayHello( ) ; //輸出:Hello Tom
【分析】
當前的這種方式,在編碼規範的情況下,是能夠正常工作的,但是,從程式的封裝的角度來看,卻存在明顯的不足。
因為,tomObject也可以設定它的getName函數,
例如:在tomObject.sayHello();之前添加如下代碼:
//....
tomObject.getName = function(){ return ‘Jack‘ };
//調用公開的方法
tomObject.sayHello( ) ; //輸出:Hello Jack
而實際上,作為一個約定,我們希望getName就是調用當前對象的name的屬性值,不允許繼承它的子物件任意覆蓋它!也就是說,getName應該是一個私人函數!
現在,我們看如何用【閉包】來解決這個問題:
1 var createPersonObjFn = function(){ 2 var that = {}; 3 4 var name = ‘Lily‘ ; 5 6 var getName = function(){ 7 return name ; 8 }; 9 10 that.setName = function( new_name ){11 name = new_name ;12 };13 that.sayHello = function(){14 console.log( ‘Hello ‘ + getName() );15 };16 17 return that ;18 };19 20 //建立一個對象21 var tomObject = createPersonObjFn();22 tomObject.setName( ‘Tom‘ );23 24 //調用公開的方法25 tomObject.sayHello( ) ; //輸出:Hello Tom
【分析】
現在好了,儘管你還是可以給tomObject增加新的getName()函數,但並不會影響sayHello的商務邏輯。同理,
//...
tomObject.setName( ‘Tom‘ );
tomObject.getName = function(){return ‘Jack‘; }; //設定對象的getName的函數
//調用公開的方法
tomObject.sayHello( ) ; //依然輸出:Hello Tom
閉包的特點就是:
1. 將要‘業務對象‘的屬性儲存在‘運行時環境‘中。
2. 天然的‘原廠模式‘,要新產生一個對象,就執行一下函數。
從這也可以看出,採用‘閉包‘這種模式構建業務時,對於‘原型鏈‘的理解要求並不高,這也許是為什麼老道在它的書中對於‘原型鏈‘著墨甚少的原因吧。
【最佳化】
但是,我們知道,在業務模型中,我們還是希望能夠實現‘繼承‘的效果,也就是說,"主體對象"實現基本的架構和邏輯,"子物件"根據自身的特點來自訂一些特定的行為。通過Object.create() 建立對象時,基於"原型鏈"的特徵,我們很好理解,只要在新建立的對象中重新定義一下自訂函數就可以了。但是,同樣的業務訴求,在‘閉包‘這種方式下如何?呢?
[方法]
在閉包對外公開的函數中,調用通過this調用的函數,那麼這個函數的行為就可以在閉包之外被自訂。
實驗代碼如下:
1 that.sayHello = function(){ 2 //這裡的sayHello調用了當前對象的getNewName() 3 console.log( ‘Hello ‘ + this.getNewName() ); 4 }; 5 6 //...前面其他的代碼不變 7 var tomObject = createPersonObjFn(); 8 tomObject.getNewName = function(){ //定義當前對象的getNewName, 9 return ‘Jack‘ ;10 }11 12 //調用公開的方法13 tomObject.sayHello( ) ; //輸出:Hello Jack
【分析】
雖然通過修改sayHello中的定義(通過調用方法函數),我們似乎能夠自訂對象的一些行為,但是,新定義的行為並不能訪問到tomObject的私人屬性name!這和對象原來想表達的內容完全沒有關係。而我們真實的業務訴求或許是這樣,自訂行為之後,sayHello 能夠列印"Hello dear Tom!" 或者"Hello my Tom!" 的內容。
[回顧]我們知道,在閉包中,如果要想訪問私人屬性,必須要定義相關的公開的方法。所以,我們最佳化如下:
1 //...在閉包中,將getName這樣的函數由私人函數轉換為公開函數 2 that.getName = function( ){ 3 return name ; 4 } 5 6 //...定義tomObject的自訂函數getNewName,在函數中調用getName的方法。 7 tomObject.getNewName = function(){ 8 return ‘dear ‘ + tomObject.getName() + ‘!‘ ; 9 }10 tomObject.setName( ‘Tom‘ );11 12 //調用公開的方法13 tomObject.sayHello( ) ; //輸出:Hello dear Tom!14 15 16 //為了體現自訂行為的特點,我們再建立另外一個Jack的對象17 var jackObject = createPersonObjFn();18 jackObject.getNewName = function(){ //定義當前對象的getNewName, 19 return ‘my ‘ + jackObject.getName() + ‘!‘ ;20 }21 jackObject.setName( ‘Jack‘ );22 23 //調用公開的方法24 jackObject.sayHello( ) ; //輸出:Hello my Jack!
【分析】
看起來似乎沒有什麼問題了,但是,還有一個小細節需要最佳化。我們在sayHello中調用了this.getNewName();但是,如果新建立的對象沒有重新定義getNewName函數,
那樣豈不報異常了?所以,嚴謹的做法應該是,在閉包中也設定一個that.getNewName的函數,預設的行為就是返回當前的name值,
如果要進行自訂行為,則對象會體現出自訂的行為,覆蓋(重載)預設的行為。
【完整的例子】
1. 在閉包中,可以定義私人屬性(指:對象、字串、數字、布爾類型等),這些屬性只能通過閉包開放的函數訪問、修改。
2. 有些函數,你並不希望外部對象對它進行調用,僅僅供閉包內的函數(包括:公開函數和私人函數)調用,則可以將它定義為私人函數。
3. 如果要想閉包對象的某一部分行為可以自訂(達到繼承的效果),則需要進行如下幾步。
a. 新增能訪問私人屬性的公開函數,例如:例子中的getName函數。
因為根據範圍的特點,閉包外部是無法訪問到私人屬性的,而自訂的函數是在閉包外部的。
b. 在閉包內部,以公開函數的方式,設定需要自訂函數的預設行為,例如:閉包中getNewName函數的定義。
c. 在允許自訂行為的公開函數(例如:例子中的sayHello函數)中,通過this調用可以自訂行為的函數。
例如例子中的this.getNewName()。
完整的代碼如下:
1 var createPersonObjFn = function(){ 2 var that = {}; 3 4 var name = ‘Lily‘ ; 5 6 that.getName = function(){ 7 return name ; 8 }; 9 that.setName = function( new_name ){10 name = new_name ;11 };12 that.getNewName = function( ){ //預設的行為13 return name ;14 };15 that.sayHello = function(){16 console.log( ‘Hello ‘ + this.getNewName() );17 };18 19 return that ;20 };21 22 //1. 建立一個對象23 var tomObject = createPersonObjFn();24 tomObject.getNewName = function(){25 return ‘dear ‘ + tomObject.getName() + ‘!‘ ;26 }27 tomObject.setName( ‘Tom‘ );28 29 //調用公開的方法30 tomObject.sayHello( ) ; //輸出:Hello dear Tom!31 32 //2. 建立另外一個Jack的對象33 var jackObject = createPersonObjFn();34 jackObject.getNewName = function(){ //定義當前對象的getNewName, 35 return ‘my ‘ + jackObject.getName() + ‘!‘ ;36 }37 jackObject.setName( ‘Jack‘ );38 39 //調用公開的方法40 jackObject.sayHello( ) ; //輸出:Hello my Jack!41 42 43 //3 建立另外一個Bill的對象,不重新定義getNewName函數,採用預設的行為44 var billObject = createPersonObjFn();45 billObject.setName( ‘Bill‘ );46 47 //調用公開的方法48 billObject.sayHello( ) ; //輸出:Hello Bill
【總結】
JavaScript是一個表現力很強的語言,非常的靈活,自然也比較容易出錯。上面舉的例子中,我們僅僅突出展現了閉包的特性,其實,利用“原型鏈”的特性,我們完全可以基於tomObject,jackObject這些對象再來建立另外的對象,或者tomObject這些對象的建立過程,放到另外一個閉包中,這樣或許可以組合出更加豐富的模型。閉包的特性就在這裡,原型鏈的特性也在這裡......到底什麼時候用?怎麼組合起來用?關鍵還是看我們的業務訴求,看真實的使用情境,看我們對效能,擴充性,安全等等多個方面的期望。
另外,本文涉及到一些背景知識,例如:原型鏈是怎樣的一個圖譜關係?new這個運算子在建立對象時都做了啥?Object.create又可以如何理解? 由於篇幅有限,就沒有展開來講,如有疑問或建議,歡迎指出討論,謝謝。
【再思考】
細心的同學或許發現了,既然閉包中that.getNewName和that.getName的實現都完全一樣,為什麼要重複定義這兩個函數呢?是不是可以把閉包中that.getName給刪除掉呢?
答案當然是否定的。如果刪除了閉包中的that.getName,而你又重新定義了that.getNewName的方法,這時候,閉包中的私人屬性name在閉包外就沒法訪問到了。
這就像同一包紙巾中的紙,樣子完全一樣,但職責不同,有些是事前用的,有些則是事後用的。
比如,你在公園裡吃蘋果,沒有水果刀,你會先抽出一張紙(A)擦一下蘋果的外表,吃完蘋果之後,把蘋果的核用紙包起來扔到垃圾桶,又抽出一張紙(B)擦一下嘴巴和手。
因為大家都是講衛生,懂文明的"四有新人"。
今天的分享到此為止,感謝大家捧場,希望諸位大俠不吝賜教。
業務建模 之 閑話'閉包'與'原型繼承'