在業務建模中,咱們常常遇到這樣一種狀況:「原型」對象負責實現業務的基本訴求(包括:有哪些屬性,有哪些函數以及它們之間的關係),以「原型」對象爲基礎建立的「子對象」則實現一些個性化的業務特性,從而方便的實現業務擴展。最多見的搞法是: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)擦一下嘴巴和手。由於你們都是講衛生,懂文明的"四有新人"。今天的分享到此爲止,感謝你們捧場,但願諸位大俠不吝賜教。