業務建模 之 閒話'閉包'與'原型繼承'

在業務建模中,咱們常常遇到這樣一種狀況:「原型」對象負責實現業務的基本訴求(包括:有哪些屬性,有哪些函數以及它們之間的關係),以「原型」對象爲基礎建立的「子對象」則實現一些個性化的業務特性,從而方便的實現業務擴展。最多見的搞法是:html

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控件自定義開發中,咱們也是這麼作的。可是,若是業務模型比較複雜,那麼上面的這種方式的弊端也是明顯的:this

沒有私有環境,全部的屬性都是公開的。編碼

 

今天,咱們就業務建模出發,看看若是藉助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)擦一下嘴巴和手。由於你們都是講衛生,懂文明的"四有新人"。今天的分享到此爲止,感謝你們捧場,但願諸位大俠不吝賜教。

相關文章
相關標籤/搜索