// 第一種方式let obj = {};// 第二種方式let obj2 = Object.create( null );// 第三種方式let obj3 = new Object();
// 1. 「點號」法// 設置屬性obj.firstKey = "Hello World";// 獲取屬性let key = obj.firstKey;// 2. 「方括號」法// 設置屬性obj["firstKey"] = "Hello World";// 獲取屬性let key = newObject["firstKey"];// 方法1和2的區別在於用方括號的方式內能夠寫表達式// 3. Object.defineProperty方式// 設置屬性Object.defineProperty(obj, "firstKey", { value: "hello world",// 屬性的值,默認爲undefined writable: true, // 是否可修改,默認爲false enumerable: true,// 是否可枚舉(遍歷),默認爲false configurable: true // 表示對象的屬性是否能夠被刪除,以及除 value 和 writable 特性外的其餘特性是否能夠被修改。});// 若是上面的方式你感到難以閱讀,能夠簡短的寫成下面這樣:let defineProp = function ( obj, key, value ){ let config = {}; config.value = value; Object.defineProperty( obj, key, config );};// 4. Object.defineProperties方式(同時設置多個屬性)// 設置屬性Object.defineProperties( obj, { "firstKey": { value: "Hello World", writable: true }, "secondKey": { value: "Hello World2", writable: false }});
Javascript不支持類的概念,但它有一種與對象一塊兒工做的構造器函數。使用new關鍵字來調用該函數,咱們能夠告訴Javascript把這個函數當作一個構造器來用,它能夠用本身所定義的成員來初始化一個對象。javascript
在這個構造器內部,關鍵字this引用到剛被建立的對象。回到對象建立,一個基本的構造函數看起來像這樣:html
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; this.toString = function () { return this.model + " has done " + this.miles + " miles"; };}// 使用:// 咱們能夠示例化一個Carlet civic = new Car( "Honda Civic", 2009, 20000 );let mondeo = new Car( "Ford Mondeo", 2010, 5000 );// 打開瀏覽器控制檯查看這些對象toString()方法的輸出值// output of the toString() method being called on// these objectsconsole.log( civic.toString() );console.log( mondeo.toString() );
上面是簡單版本的構造器模式,但它仍是有些問題。一個是難以繼承,另外一個是每一個Car構造函數建立的對象中,toString()之類的函數都被從新定義。這不是很是好,理想的狀況是全部Car類型的對象都應該引用同一個函數。 java
在Javascript中函數有一個prototype的屬性。當咱們調用Javascript的構造器建立一個對象時,構造函數prototype上的屬性對於所建立的對象來講都看見。照這樣,就能夠建立多個訪問相同prototype的Car對象了。下面,咱們來擴展一下原來的例子:數據庫
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles;}Car.prototype.toString = function () { return this.model + " has done " + this.miles + " miles";};// 使用:var civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 );console.log( civic.toString() );console.log( mondeo.toString() );
經過上面代碼,單個toString()實例被全部的Car對象所共享了。編程
模塊是任何健壯的應用程序體系結構不可或缺的一部分,特色是有助於保持應用項目的代碼單元既能清晰地分離又有組織。設計模式
在JavaScript中,實現模塊有幾個選項,他們包括:數組
模塊化模式瀏覽器
對象表示法安全
AMD模塊數據結構
Commonjs 模塊
ECMAScript Harmony 模塊
對象字面值不要求使用新的操做實例,可是不可以在結構體開始使用,由於打開"{"可能被解釋爲一個塊的開始。
let myModule = { myProperty: "someValue", // 對象字面值包含了屬性和方法(properties and methods). // 例如,咱們能夠定義一個模塊配置進對象: myConfig: { useCaching: true, language: "en" }, // 很是基本的方法 myMethod: function () { console.log( "Where in the world is Paul Irish today?" ); }, // 輸出基於當前配置configuration的一個值 myMethod2: function () { console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" ); }, // 重寫當前的配置(configuration) myMethod3: function( newConfig ) { if ( typeof newConfig === "object" ) { this.myConfig = newConfig; console.log( this.myConfig.language ); } }};myModule.myMethod();// Where in the world is Paul Irish today?myModule.myMethod2();// enabledmyModule.myMethod3({ language: "fr", useCaching: false});// fr
模塊化模式最初被定義爲一種對傳統軟件工程中的類提供私有和公共封裝的方法。
在JavaScript中,模塊化模式用來進一步模擬類的概念,經過這樣一種方式:咱們能夠在一個單一的對象中包含公共/私有的方法和變量,從而從全局範圍中屏蔽特定的部分。
這個結果是能夠減小咱們的函數名稱與在頁面中其餘腳本區域定義的函數名稱衝突的可能性。
模塊模式使用閉包的方式來將"私有信息",狀態和組織結構封裝起來。提供了一種將公有和私有方法,變量封裝混合在一塊兒的方式,這種方式防止內部信息泄露到全局中,從而避免了和其它開發者接口發生衝圖的可能性。
在這種模式下只有公有的API 會返回,其它將所有保留在閉包的私有空間中。
這種方法提供了一個比較清晰的解決方案,在只暴露一個接口供其它部分使用的狀況下,將執行繁重任務的邏輯保護起來。這個模式很是相似於當即調用函數式表達式(IIFE-查看命名空間相關章節獲取更多信息),可是這種模式返回的是對象,而當即調用函數表達式返回的是一個函數。
須要注意的是,在javascript事實上沒有一個顯式的真正意義上的"私有性"概念,由於與傳統語言不一樣,javascript沒有訪問修飾符。從技術上講,變量不能被聲明爲公有的或者私有的,所以咱們使用函數域的方式去模擬這個概念。
在模塊模式中,由於閉包的緣故,聲明的變量或者方法只在模塊內部有效。在返回對象中定義的變量或者方法能夠供任何人使用。
let testModule = (function () { let counter = 0; return { incrementCounter: function () { return counter++; }, resetCounter: function () { console.log( "counter value prior to reset: " + counter ); counter = 0; } };})();testModule.incrementCounter();testModule.resetCounter();
在這裏咱們看到,其它部分的代碼不能直接訪問咱們的incrementCounter() 或者 resetCounter()的值。counter變量被徹底從全局域中隔離起來了,所以其表現的就像一個私有變量同樣,它的存在只侷限於模塊的閉包內部,所以只有兩個函數能夠訪問counter。
咱們的方法是有名字空間限制的,所以在咱們代碼的測試部分,咱們須要給全部函數調用前面加上模塊的名字(例如"testModule")。
當使用模塊模式時,咱們會發現經過使用簡單的模板,對於開始使用模塊模式很是有用。下面是一個模板包含了命名空間,公共變量和私有變量。
let myNamespace = (function () { let myPrivateVar, myPrivateMethod; myPrivateVar = 0; myPrivateMethod = function( foo ) { console.log( foo ); }; return { myPublicVar: "foo", myPublicFunction: function( bar ) { myPrivateVar++; myPrivateMethod( bar ); } };})();
看一下另一個例子,下面咱們看到一個使用這種模式實現的購物車。這個模塊徹底自包含在一個叫作basketModule 全局變量中。
模塊中的購物車數組是私有的,應用的其它部分不能直接讀取。只存在與模塊的閉包中,所以只有能夠訪問其域的方法能夠訪問這個變量。
let basketModule = (function () { let basket = []; function doSomethingPrivate() { //... } function doSomethingElsePrivate() { //... } return { addItem: function( values ) { basket.push(values); }, getItemCount: function () { return basket.length; }, doSomething: doSomethingPrivate, getTotal: function () { let q = this.getItemCount(), p = 0; while (q--) { p += basket[q].price; } return p; } };}());
上面的方法都處於basketModule 的名字空間中。
請注意在上面的basket模塊中 域函數是如何在咱們全部的函數中被封裝起來的,以及咱們如何當即調用這個域函數,而且將返回值保存下來。這種方式有如下的優點:
能夠建立只能被咱們模塊訪問的私有函數。這些函數沒有暴露出來(只有一些API是暴露出來的),它們被認爲是徹底私有的。
當咱們在一個調試器中,須要發現哪一個函數拋出異常的時候,能夠很容易的看到調用棧,由於這些函數是正常聲明的而且是命名的函數。
這種模式一樣可讓咱們在不一樣的狀況下返回不一樣的函數。我見過有開發者使用這種技巧用於執行測試,目的是爲了在他們的模塊裏面針對IE專門提供一條代碼路徑,可是如今咱們也能夠簡單的使用特徵檢測達到相同的目的。
這個變體展現瞭如何將全局(例如 jQuery, Underscore)做爲一個參數傳入模塊的匿名函數。這種方式容許咱們導入全局,而且按照咱們的想法在本地爲這些全局起一個別名。
let myModule = (function ( jQ, _ ) { function privateMethod1(){ jQ(".container").html("test"); } function privateMethod2(){ console.log( _.min([10, 5, 100, 2, 1000]) ); } return{ publicMethod: function(){ privateMethod1(); } };}( jQuery, _ ));// 將JQ和lodash導入myModule.publicMethod();
這個變體容許咱們聲明全局對象而不用使用它們。
let myModule = (function () { let module = {}, privateVariable = "Hello World"; function privateMethod() { // ... } module.publicProperty = "Foobar"; module.publicMethod = function () { console.log( privateVariable ); }; return module;}());
Dojo:
Dojo提供了一個方便的方法 dojo.setObject() 來設置對象。這須要將以"."符號爲第一個參數的分隔符,如:myObj.parent.child 是指定義在"myOjb"內部的一個對象「parent」,它的一個屬性爲"child"。
使用setObject()方法容許咱們設置children 的值,能夠建立路徑傳遞過程當中的任何對象即便這些它們根本不存在。
例如,若是咱們聲明商店命名空間的對象basket.coreas,可使用以下方式:
let store = window.store || {};
if ( !store["basket"] ) { store.basket = {};}
if ( !store.basket["core"] ) { store.basket.core = {};}
store.basket.core = { key:value,};
Extjs:
// create namespaceExt.namespace("myNameSpace");// create applicationmyNameSpace.app = function () { // do NOT access DOM from here; elements don't exist yet // private variables let btn1, privVar1 = 11; // private functions let btn1Handler = function ( button, event ) { console.log( "privVar1=" + privVar1 ); console.log( "this.btn1Text=" + this.btn1Text ); }; // public space return { // public properties, e.g. strings to translate btn1Text: "Button 1", // public methods init: function () { if ( Ext.Ext2 ) { btn1 = new Ext.Button({ renderTo: "btn1-ct", text: this.btn1Text, handler: btn1Handler }); } else { btn1 = new Ext.Button( "btn1-ct", { text: this.btn1Text, handler: btn1Handler }); } } };}();
jQuery:
由於jQuery編碼規範沒有規定插件如何實現模塊模式,所以有不少種方式能夠實現模塊模式。Ben Cherry 之間提供一種方案,由於模塊之間可能存在大量的共性,所以經過使用函數包裝器封裝模塊的定義。
在下面的例子中,定義了一個library 函數,這個函數聲明瞭一個新的庫,而且在新的庫(例如 模塊)建立的時候,自動將初始化函數綁定到document的ready上。
function library( module ) { $( function() { if ( module.init ) { module.init(); } }); return module;}let myLibrary = library(function () { return { init: function () { // module implementation } };}());
優勢:
既然咱們已經看到單例模式頗有用,爲何仍是使用模塊模式呢?首先,對於有面向對象背景的開發者來說,至少從javascript語言上來說,模塊模式相對於真正的封裝概念更清晰。
其次,模塊模式支持私有數據-所以,在模塊模式中,公共部分代碼能夠訪問私有數據,可是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。
缺點:
模塊模式的缺點是由於咱們採用不一樣的方式訪問公有和私有成員,所以當咱們想要改變這些成員的可見性的時候,咱們不得不在全部使用這些成員的地方修改代碼。
咱們也不能在對象以後添加的方法裏面訪問這些私有變量。也就是說,不少狀況下,模塊模式頗有用,而且當使用正確的時候,潛在地能夠改善咱們代碼的結構。
其它缺點包括不能爲私有成員建立自動化的單元測試,以及在緊急修復bug時所帶來的額外的複雜性。根本沒有可能能夠對私有成員打補丁。
相反地,咱們必須覆蓋全部的使用存在bug私有成員的公共方法。開發者不能簡單的擴展私有成員,所以咱們須要記得,私有成員並不是它們表面上看上去那麼具備擴展性。
單例模式之因此這麼叫,是由於它限制一個類只能有一個實例化對象。經典的實現方式是,建立一個類,這個類包含一個方法,這個方法在沒有對象存在的狀況下,將會建立一個新的實例對象。若是對象存在,這個方法只是返回這個對象的引用。
在JavaScript語言中, 單例服務做爲一個從全局空間的代碼實現中隔離出來共享的資源空間是爲了提供一個單獨的函數訪問指針。
咱們能像這樣實現一個單例:
let mySingleton = (function () { // Instance stores a reference to the Singleton let instance; function init() { // 單例 // 私有方法和變量 function privateMethod(){ console.log( "I am private" ); } let privateVariable = "Im also private"; let privateRandomNumber = Math.random(); return { // 共有方法和變量 publicMethod: function () { console.log( "The public can see me!" ); }, publicProperty: "I am also public", getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 若是存在獲取此單例實例,若是不存在建立一個單例實例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } };})();
let myBadSingleton = (function () { // 存儲單例實例的引用 var instance; function init() { // 單例 let privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 老是建立一個新的實例 getInstance: function () { instance = init(); return instance; } };})();
// 使用:let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true
let badSingleA = myBadSingleton.getInstance();let badSingleB = myBadSingleton.getInstance();console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true
建立一個全局訪問的單例實例 (一般經過 MySingleton.getInstance()) 由於咱們不能(至少在靜態語言中) 直接調用 new MySingleton() 建立實例. 這在JavaScript語言中是不可能的。
在GoF的書裏面,單例模式的應用描述以下:
每一個類只有一個實例,這個實例必須經過一個廣爲人知的接口,來被客戶訪問。
子類若是要擴展這個惟一的實例,客戶能夠不用修改代碼就能使用這個擴展後的實例。
關於第二點,能夠參考以下的實例,咱們須要這樣編碼:
mySingleton.getInstance = function(){ if ( this._instance == null ) { if ( isFoo() ) { this._instance = new FooSingleton(); } else { this._instance = new BasicSingleton(); } } return this._instance;};
在這裏,getInstance 有點相似於工廠方法,咱們不須要去更新每一個訪問單例的代碼。FooSingleton能夠是BasicSinglton的子類,而且實現了相同的接口。
儘管單例模式有着合理的使用需求,可是一般當咱們發現本身須要在javascript使用它的時候,這是一種信號,代表咱們可能須要去從新評估本身的設計。
這一般代表系統中的模塊要麼緊耦合要麼邏輯過於分散在代碼庫的多個部分。單例模式更難測試,由於可能有多種多樣的問題出現,例如隱藏的依賴關係,很難去建立多個實例,很難清理依賴關係,等等。
觀察者模式是這樣一種設計模式:一個被稱做被觀察者的對象,維護一組被稱爲觀察者的對象,這些對象依賴於被觀察者,被觀察者自動將自身的狀態的任何變化通知給它們。
當一個被觀察者須要將一些變化通知給觀察者的時候,它將採用廣播的方式,這條廣播可能包含特定於這條通知的一些數據。
當特定的觀察者再也不須要接受來自於它所註冊的被觀察者的通知的時候,被觀察者能夠將其從所維護的組中刪除。在這裏說起一下設計模式現有的定義頗有必要。這個定義是與所使用的語言無關的。
經過這個定義,最終咱們能夠更深層次地瞭解到設計模式如何使用以及其優點。在gof的《設計模式:可重用的面向對象軟件的元素》這本書中,是這樣定義觀察者模式的:
一個或者更多的觀察者對一個被觀察者的狀態感興趣,將自身的這種興趣經過附着自身的方式註冊在被觀察者身上。當被觀察者發生變化,而這種即可也是觀察者所關心的,就會產生一個通知,這個通知將會被送出去,最後將會調用每一個觀察者的更新方法。當觀察者不在對被觀察者的狀態感興趣的時候,它們只須要簡單的將自身剝離便可。
咱們如今能夠經過實現一個觀察者模式來進一步擴展咱們剛纔所學到的東西。這個實現包含一下組件:
被觀察者:維護一組觀察者, 提供用於增長和移除觀察者的方法。
觀察者:提供一個更新接口,用於當被觀察者狀態變化時,獲得通知。
具體的被觀察者:狀態變化時廣播通知給觀察者,保持具體的觀察者的信息。
具體的觀察者:保持一個指向具體被觀察者的引用,實現一個更新接口,用於觀察,以便保證自身狀態老是和被觀察者狀態一致的。
首先,讓咱們對被觀察者可能有的一組依賴其的觀察者進行建模:
function ObserverList(){ this.observerList = [];}ObserverList.prototype.Add = function( obj ){ return this.observerList.push( obj );};ObserverList.prototype.Empty = function(){ this.observerList = [];};ObserverList.prototype.Count = function(){ return this.observerList.length;};ObserverList.prototype.Get = function( index ){ if( index > -1 && index < this.observerList.length ){ return this.observerList[ index ]; }};ObserverList.prototype.Insert = function( obj, index ){ let pointer = -1; if( index === 0 ){ this.observerList.unshift( obj ); pointer = index; }else if( index === this.observerList.length ){ this.observerList.push( obj ); pointer = index; } return pointer;};ObserverList.prototype.IndexOf = function( obj, startIndex ){ let i = startIndex, pointer = -1; while( i < this.observerList.length ){ if( this.observerList[i] === obj ){ pointer = i; } i++; } return pointer;};ObserverList.prototype.RemoveAt = function( index ){ if( index === 0 ){ this.observerList.shift(); }else if( index === this.observerList.length -1 ){ this.observerList.pop(); }};// Extend an object with an extensionfunction extend( extension, obj ){ for ( let key in extension ){ obj[key] = extension[key]; }}
接着,咱們對被觀察者以及其增長,刪除,通知在觀察者列表中的觀察者的能力進行建模:
function Subject(){ this.observers = new ObserverList();}Subject.prototype.AddObserver = function( observer ){ this.observers.Add( observer );}; Subject.prototype.RemoveObserver = function( observer ){ this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );}; Subject.prototype.Notify = function( context ){ let observerCount = this.observers.Count(); for(let i=0; i < observerCount; i++){ this.observers.Get(i).Update( context ); }};
咱們接着定義創建新的觀察者的一個框架。這裏的update 函數以後會被具體的行爲覆蓋。
// The Observerfunction Observer(){ this.Update = function(){ // ... };}
在咱們的樣例應用裏面,咱們使用上面的觀察者組件,如今咱們定義:
一個按鈕,這個按鈕用於增長新的充當觀察者的選擇框到頁面上
一個控制用的選擇框 , 充當一個被觀察者,通知其它選擇框是否應該被選中
一個容器,用於放置新的選擇框
咱們接着定義具體被觀察者和具體觀察者,用於給頁面增長新的觀察者,以及實現更新接口。經過查看下面的內聯的註釋,搞清楚在咱們樣例中的這些組件是如何工做的。
html
<button id="addNewObserver">Add New Observer checkbox</button><input id="mainCheckbox" type="checkbox"/><div id="observersContainer"></div>
Javascript
// 咱們DOM 元素的引用let controlCheckbox = document.getElementById("mainCheckbox"), addBtn = document.getElementById( "addNewObserver" ), container = document.getElementById( "observersContainer" );// 具體的被觀察者//Subject 類擴展controlCheckbox 類extend( new Subject(), controlCheckbox );//點擊checkbox 將會觸發對觀察者的通知controlCheckbox["onclick"] = new Function("controlCheckbox.Notify(controlCheckbox.checked)");addBtn["onclick"] = AddNewObserver;// 具體的觀察者function AddNewObserver(){ //創建一個新的用於增長的checkbox let check = document.createElement( "input" ); check.type = "checkbox"; // 使用Observer 類擴展checkbox extend( new Observer(), check ); // 使用定製的Update函數重載 check.Update = function( value ){ this.checked = value; }; // 增長新的觀察者到咱們主要的被觀察者的觀察者列表中 controlCheckbox.AddObserver( check ); // 將元素添加到容器的最後 container.appendChild( check );}
在這個例子裏面,咱們看到了如何實現和配置觀察者模式,瞭解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。
觀察者模式和發佈/訂閱模式的不一樣
觀察者模式確實頗有用,可是在javascript時間裏面,一般咱們使用一種叫作發佈/訂閱模式的變體來實現觀察者模式。這兩種模式很類似,可是也有一些值得注意的不一樣。
觀察者模式要求想要接受相關通知的觀察者必須到發起這個事件的被觀察者上註冊這個事件。
發佈/訂閱模式使用一個主題/事件頻道,這個頻道處於想要獲取通知的訂閱者和發起事件的發佈者之間。
這個事件系統容許代碼定義應用相關的事件,這個事件能夠傳遞特殊的參數,參數中包含有訂閱者所須要的值。這種想法是爲了不訂閱者和發佈者之間的依賴性。
這種和觀察者模式之間的不一樣,使訂閱者能夠實現一個合適的事件處理函數,用於註冊和接受由發佈者廣播的相關通知。
這裏給出一個關於如何使用發佈者/訂閱者模式的例子,這個例子中完整地實現了功能強大的publish(), subscribe() 和 unsubscribe()。
// 一個很是簡單的郵件處理器// 接受的消息的計數器let mailCounter = 0;// 初始化一個訂閱者,這個訂閱者監聽名叫"inbox/newMessage" 的頻道// 渲染新消息的粗略信息let subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) { // 日誌記錄主題,用於調試 console.log( "A new message was received: ", topic ); // 使用來自於被觀察者的數據,用於給用戶展現一個消息的粗略信息 $( ".messageSender" ).html( data.sender ); $( ".messagePreview" ).html( data.body );});// 這是另一個訂閱者,使用相同的數據執行不一樣的任務// 更細計數器,顯示當前來自於發佈者的新信息的數量let subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) { $('.newMessageCounter').html( mailCounter++ );});publish( "inbox/newMessage", [{ sender:"hello@google.com", body: "Hey there! How are you doing today?"}]);// 在以後,咱們可讓咱們的訂閱者經過下面的方式取消訂閱來自於新主題的通知// unsubscribe( subscriber1, );// unsubscribe( subscriber2 );
這個例子的更廣的意義是對鬆耦合的原則的一種推崇。不是一個對象直接調用另一個對象的方法,而是經過訂閱另一個對象的一個特定的任務或者活動,從而在這個任務或者活動出現的時候的獲得通知。
優勢
觀察者和發佈/訂閱模式鼓勵人們認真考慮應用不一樣部分之間的關係,同時幫助咱們找出這樣的層,該層中包含有直接的關係,這些關係能夠經過一些列的觀察者和被觀察者來替換掉。
這中方式能夠有效地將一個應用程序切割成小塊,這些小塊耦合度低,從而改善代碼的管理,以及用於潛在的代碼複用。
使用觀察者模式更深層次的動機是,當咱們須要維護相關對象的一致性的時候,咱們能夠避免對象之間的緊密耦合。例如,一個對象能夠通知另一個對象,而不須要知道這個對象的信息。
兩種模式下,觀察者和被觀察者之間均可以存在動態關係。這提供很好的靈活性,而當咱們的應用中不一樣的部分之間緊密耦合的時候,是很難實現這種靈活性的。
儘管這些模式並非萬能的靈丹妙藥,這些模式仍然是做爲最好的設計鬆耦合系統的工具之一,所以在任何的JavaScript 開發者的工具箱裏面,都應該有這樣一個重要的工具。
缺點
事實上,這些模式的一些問題實際上正是來自於它們所帶來的一些好處。在發佈/訂閱模式中,將發佈者共訂閱者上解耦,將會在一些狀況下,致使很難確保咱們應用中的特定部分按照咱們預期的那樣正常工做。
例如,發佈者能夠假設有一個或者多個訂閱者正在監聽它們。好比咱們基於這樣的假設,在某些應用處理過程當中來記錄或者輸出錯誤日誌。若是訂閱者執行日誌功能崩潰了(或者由於某些緣由不能正常工做),由於系統自己的解耦本質,發佈者沒有辦法感知到這些事情。
另一個這種模式的缺點是,訂閱者對彼此之間存在沒有感知,對切換髮布者的代價無從得知。由於訂閱者和發佈者之間的動態關係,更新依賴也很能去追蹤。
讓咱們看一下最小的一個版本的發佈/訂閱模式實現。這個實現展現了發佈,訂閱的核心概念,以及如何取消訂閱。
let pubsub = {};(function(q) { let topics = {}, subUid = -1; q.publish = function( topic, args ) { if ( !topics[topic] ) { return false; } let subscribers = topics[topic], len = subscribers ? subscribers.length : 0; while (len--) { subscribers[len].func( topic, args ); } return this; }; q.subscribe = function( topic, func ) { if (!topics[topic]) { topics[topic] = []; } let token = ( ++subUid ).toString(); topics[topic].push({ token: token, func: func }); return token; }; q.unsubscribe = function( token ) { for ( let m in topics ) { if ( topics[m] ) { for ( let i = 0, j = topics[m].length; i < j; i++ ) { if ( topics[m][i].token === token) { topics[m].splice( i, 1 ); return token; } } } } return this; };}( pubsub ));
咱們如今可使用發佈實例和訂閱感興趣的事件,例如:
let messageLogger = function ( topics, data ) { console.log( "Logging: " + topics + ": " + data );};let subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );pubsub.publish( "inbox/newMessage", "hello world!" );// orpubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );// orpubsub.publish( "inbox/newMessage", { sender: "hello@google.com", body: "Hey again!"});// We cab also unsubscribe if we no longer wish for our subscribers// to be notified// pubsub.unsubscribe( subscription );pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );
觀察者模式在應用設計中,解耦一系列不一樣的場景上很是有用,若是你沒有用過它,我推薦你嘗試一下今天提到的以前寫到的某個實現。這個模式是一個易於學習的模式,同時也是一個威力巨大的模式。
若是系統組件之間存在大量的直接關係,就多是時候,使用一箇中心的控制點,來讓不一樣的組件經過它來通訊。中介者經過將組件之間顯式的直接的引用替換成經過中心點來交互的方式,來作到鬆耦合。這樣能夠幫助咱們解耦,和改善組件的重用性。
在現實世界中,相似的系統就是,飛行控制系統。一個航站塔(中介者)處理哪一個飛機能夠起飛,哪一個能夠着陸,由於全部的通訊(監聽的通知或者廣播的通知)都是飛機和控制塔之間進行的,而不是飛機和飛機之間進行的。一箇中央集權的控制中心是這個系統成功的關鍵,也正是中介者在軟件設計領域中所扮演的角色。
中間人模式的一種簡單的實現能夠在下面找到,publish()和subscribe()方法都被暴露出來使用:
let mediator = (function(){ let topics = {}; let subscribe = function( topic, fn ){ if ( !topics[topic] ){ topics[topic] = []; } topics[topic].push( { context: this, callback: fn } ); return this; }; let publish = function( topic ){ let args; if ( !topics[topic] ){ return false; } args = Array.prototype.slice.call( arguments, 1 ); for ( let i = 0, l = topics[topic].length; i < l; i++ ) { let subscription = topics[topic][i]; subscription.callback.apply( subscription.context, args ); } return this; }; return { publish: publish, subscribe: subscribe, installTo: function( obj ){ obj.subscribe = subscribe; obj.publish = publish; } };}());
優勢 & 缺點
中間人模式最大的好處就是,它節約了對象或者組件之間的通訊信道,這些對象或者組件存在於從多對多到多對一的系統之中。因爲解耦合水平的因素,添加新的發佈或者訂閱者是相對容易的。
也許使用這個模式最大的缺點是它能夠引入一個單點故障。在模塊之間放置一箇中間人也可能會形成性能損失,由於它們常常是間接地的進行通訊的。因爲鬆耦合的特性,僅僅盯着廣播很難去確認系統是如何作出反應的。
這就是說,提醒咱們本身解耦合的系統擁有許多其它的好處,是頗有用的——若是咱們的模塊互相之間直接的進行通訊,對於模塊的改變(例如:另外一個模塊拋出了異常)能夠很容易的對咱們系統的其它部分產生多米諾連鎖效應。這個問題在解耦合的系統中不多須要被考慮到。
在一天結束的時候,緊耦合會致使各類頭痛,這僅僅只是另一種可選的解決方案,可是若是獲得正確實現的話也可以工做得很好。
原型模式是指經過克隆的方式基於一個現有對象的模板建立對象的模式。
咱們可以將原型模式認做是基於原型的繼承中,咱們建立做爲其它對象原型的對象.原型對象自身被當作構造器建立的每個對象的藍本高效的使用着.若是構造器函數使用的原型包含例如叫作name的屬性,那麼每個經過同一個構造器建立的對象都將擁有這個相同的屬性。
咱們能夠在下面的示例中看到對這個的展現:
let myCar = { name: "Ford Escort", drive: function () { console.log( "Weeee. I'm driving!" ); }, panic: function () { console.log( "Wait. How do you stop this thing?" ); }};let yourCar = Object.create( myCar );console.log( yourCar.name );// Ford Escort
Object.create也容許咱們簡單的繼承先進的概念,好比對象可以直接繼承自其它對象,這種不一樣的繼承.咱們早先也看到Object.create容許咱們使用 供應的第二個參數來初始化對象屬性。例如:
let vehicle = { getModel: function () { console.log( "The model of this vehicle is.." + this.model ); }};let car = Object.create(vehicle, { "id": { value: "1", // writable:false, configurable:false by default enumerable: true }, "model": { value: "Ford", enumerable: true }});
這裏的屬性能夠被Object.create的第二個參數來初始化,使用一種相似於Object.defineProperties和Object.defineProperties方法所使用語法的對象字面值。
在枚舉對象的屬性,和在一個hasOwnProperty()檢查中封裝循環的內容時,原型關係會形成麻煩,這一事實是值得咱們關注的。
若是咱們但願在不直接使用Object.create的前提下實現原型模式,咱們能夠像下面這樣,按照上面的示例,模擬這一模式:
let vehiclePrototype = { init: function ( carModel ) { this.model = carModel; }, getModel: function () { console.log( "The model of this vehicle is.." + this.model); }};function vehicle( model ) { function F() {}; F.prototype = vehiclePrototype; let f = new F(); f.init( model ); return f;}let car = vehicle( "Ford Escort" );car.getModel();
注意:這種可選的方式不容許用戶使用相同的方式定義只讀的屬性(由於若是不當心的話vehicle原型可能會被改變)。
原型模式的最後一種可選實現能夠像下面這樣:
let beget = (function () { function F() {} return function ( proto ) { F.prototype = proto; return new F(); };})();
命名模式的目標是將方法的調用,請求或者操做封裝到一個單獨的對象中,給咱們酌情執行同時參數化和傳遞方法調用的能力.另外,它使得咱們能將對象從實現了行爲的對象對這些行爲的調用進行解耦,爲咱們帶來了換出具體的對象這一更深程度的總體靈活性。
具體類是對基於類的編程語言的最好解釋,而且同抽象類的理念聯繫緊密。抽象類定義了一個接口,但並不須要提供對它的全部成員函數的實現。它扮演着驅動其它類的基類角色.被驅動類實現了缺失的函數而被稱爲具體類.。命令模式背後的通常理念是爲咱們提供了從任何執行中的命令中分離出發出命令的責任,取而代之將這一責任委託給其它的對象。
實現明智簡單的命令對象,將一個行爲和對象對調用這個行爲的需求都綁定到了一塊兒.它們始終都包含一個執行操做(好比run()或者execute()).全部帶有相同接口的命令對象可以被簡單地根據須要調換,這被認爲是命令模式的更大的好處之一。
爲了展現命令模式,咱們建立一個簡單的汽車購買服務:
(function(){ let CarManager = { requestInfo: function( model, id ){ return "The information for " + model + " with ID " + id + " is foobar"; }, buyVehicle: function( model, id ){ return "You have successfully purchased Item " + id + ", a " + model; }, arrangeViewing: function( model, id ){ return "You have successfully booked a viewing of " + model + " ( " + id + " ) "; } };})();
看一看上面的這段代碼,它也許是經過直接訪問對象來瑣碎的調用咱們CarManager的方法。在技術上咱們也許都會都會對這個沒有任何失誤達成諒解.它是徹底有效的Javascript然而也會有狀況不利的狀況。
例如,想象若是CarManager的核心API會發生改變的這種狀況.這可能須要全部直接訪問這些方法的對象也跟着被修改.這能夠被當作是一種耦合,明顯違背了OOP方法學儘可能實現鬆耦合的理念.取而代之,咱們能夠經過更深刻的抽象這些API來解決這個問題。
如今讓咱們來擴展咱們的CarManager,以便咱們這個命令模式的應用程序獲得接下來的這種效果:接受任何能夠在CarManager對象上面執行的方法,傳送任何能夠被使用到的數據,如Car模型和ID。
這裏是咱們但願可以實現的樣子:
CarManager.execute( "buyVehicle", "Ford Escort", "453543" );
按照這種結構,咱們如今應該像下面這樣,添加一個對於"CarManager.execute()"方法的定義:
CarManager.execute = function ( name ) { return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) );};
最終咱們的調用以下所示:
CarManager.execute( "arrangeViewing", "Ferrari", "14523" );CarManager.execute( "requestInfo", "Ford Mondeo", "54323" );CarManager.execute( "requestInfo", "Ford Escort", "34232" );CarManager.execute( "buyVehicle", "Ford Escort", "34232" );
當咱們提出一個門面,咱們要向這個世界展示的是一個外觀,這一外觀可能藏匿着一種很是不同凡響的真實。這就是咱們即將要回顧的模式背後的靈感——門面模式。
這一模式提供了面向一種更大型的代碼體提供了一個的更高級別的溫馨的接口,隱藏了其真正的潛在複雜性。
把這一模式想象成要是呈現給開發者簡化的API,一些老是會提高使用性能的東西。
爲了在咱們所學的基礎上進行構建,門面模式同時須要簡化一個類的接口,和把類同使用它的代碼解耦。這給予了咱們使用一種方式直接同子系統交互的能力,這一方式有時候會比直接訪問子系統更加不容易出錯。
門面的優點包括易用,還有經常實現起這個模式來只是一小段路,不費力。
讓咱們經過實踐來看看這個模式。這是一個沒有通過優化的代碼示例,可是這裏咱們使用了一個門面來簡化跨瀏覽器事件監聽的接口。咱們建立了一個公共的方法來實現,此方法可以被用在檢查特性的存在的代碼中,以便這段代碼可以提供一種安全和跨瀏覽器兼容方案。
let addMyEvent = function( el,ev,fn ){ if( el.addEventListener ){ el.addEventListener( ev,fn, false ); }else if(el.attachEvent){ el.attachEvent( "on" + ev, fn ); }else{ el["on" + ev] = fn; }};
門面不只僅只被用在它們本身身上,它們也可以被用來同其它的模式諸如模塊模式進行集成。如咱們在下面所看到的,咱們模塊模式的實體包含許多被定義爲私有的方法。門面則被用來提供訪問這些方法的更加簡單的API:
let module = (function() { let _private = { i:5, get : function() { console.log( "current value:" + this.i); }, set : function( val ) { this.i = val; }, run : function() { console.log( "running" ); }, jump: function(){ console.log( "jumping" ); } }; return { facade : function( args ) { _private.set(args.val); _private.get(); if ( args.run ) { _private.run(); } } };}());module.facade( {run: true, val:10} );// "current value: 10" and "running"
在這個示例中,調用module.facade()將會觸發一堆模塊中的私有方法。但再一次,用戶並不須要關心這些。咱們已經使得對用戶而言不須要擔憂實現級別的細節就能消受一種特性。
工廠模式是另一種關注對象建立概念的建立模式。它的領域中同其它模式的不一樣之處在於它並無明確要求咱們使用一個構造器。
取而代之,一個工廠能提供一個建立對象的公共接口,咱們能夠在其中指定咱們但願被建立的工廠對象的類型。
下面咱們經過使用構造器模式邏輯來定義汽車。這個例子展現了Vehicle 工廠可使用工廠模式來實現。
function Car( options ) { this.doors = options.doors || 4; this.state = options.state || "brand new"; this.color = options.color || "silver";
}function Truck( options){ this.state = options.state || "used"; this.wheelSize = options.wheelSize || "large"; this.color = options.color || "blue";}function VehicleFactory() {}VehicleFactory.prototype.vehicleClass = Car;VehicleFactory.prototype.createVehicle = function ( options ) { if( options.vehicleType === "car" ){ this.vehicleClass = Car; }else{ this.vehicleClass = Truck; } return new this.vehicleClass( options );
};let carFactory = new VehicleFactory();let car = carFactory.createVehicle( { vehicleType: "car", color: "yellow", doors: 6 } );console.log( car );
什麼時候使用工廠模式
當被應用到下面的場景中時,工廠模式特別有用:
當咱們的對象或者組件設置涉及到高程度級別的複雜度時。
當咱們須要根據咱們所在的環境方便的生成不一樣對象的實體時。
當咱們在許多共享同一個屬性的許多小型對象或組件上工做時。
當帶有其它僅僅須要知足一種API約定(又名鴨式類型)的對象的組合對象工做時.這對於解耦來講是有用的。
什麼時候不要去使用工廠模式
當被應用到錯誤的問題類型上時,這一模式會給應用程序引入大量沒必要要的複雜性.除非爲建立對象提供一個接口是咱們編寫的庫或者框架的一個設計上目標,不然我會建議使用明確的構造器,以免沒必要要的開銷。
因爲對象的建立過程被高效的抽象在一個接口後面的事實,這也會給依賴於這個過程可能會有多複雜的單元測試帶來問題。
抽象工廠
瞭解抽象工廠模式也是很是實用的,它的目標是以一個通用的目標將一組獨立的工廠進行封裝.它將一堆對象的實現細節從它們的通常用例中分離。
抽象工廠應該被用在一種必須從其建立或生成對象的方式處獨立,或者須要同多種類型的對象一塊兒工做,這樣的系統中。
簡單且容易理解的例子就是一個發動機工廠,它定義了獲取或者註冊發動機類型的方式。抽象工廠會被命名爲AbstractVehicleFactory。抽象工廠將容許像"car"或者"truck"的發動機類型的定義,而且構造工廠將僅實現知足發動機合同的類.(例如:Vehicle.prototype.driven和Vehicle.prototype.breakDown)。
let AbstractVehicleFactory = (function () { let types = {}; return { getVehicle: function ( type, customizations ) { var Vehicle = types[type]; return (Vehicle ? new Vehicle(customizations) : null); }, registerVehicle: function ( type, Vehicle ) { let proto = Vehicle.prototype; // only register classes that fulfill the vehicle contract if ( proto.drive && proto.breakDown ) { types[type] = Vehicle; } return AbstractVehicleFactory; } };})();
AbstractVehicleFactory.registerVehicle( "car", Car );AbstractVehicleFactory.registerVehicle( "truck", Truck );
let car = AbstractVehicleFactory.getVehicle( "car" , { color: "lime green", state: "like new" } );
let truck = AbstractVehicleFactory.getVehicle( "truck" , { wheelSize: "medium", color: "neon yellow" } );
mixin模式指一些提供可以被一個或者一組子類簡單繼承功能的類,意在重用其功能。
子類劃分
子類劃分是一個參考了爲一個新對象繼承來自一個基類或者超類對象的屬性的術語.在傳統的面向對象編程中,類B可以從另一個類A處擴展。這裏咱們將A看作是超類,而將B看作是A的子類。如此,全部B的實體都從A處繼承了其A的方法,然而B仍然可以定義它本身的方法,包括那些重載的本來在A中的定義的方法。
B是否應該調用已經被重載的A中的方法,咱們將這個引述爲方法鏈.B是否應該調用A(超類)的構造器,咱們將這稱爲構造器鏈。
爲了演示子類劃分,首先咱們須要一個可以建立自身新實體的基對象。
let Person = function( firstName , lastName ){ this.firstName = firstName; this.lastName = lastName; this.gender = "male";};
接下來,咱們將制定一個新的類(對象),它是一個現有的Person對象的子類.讓咱們想象咱們想要加入一個不一樣屬性用來分辨一個Person和一個繼承了Person"超類"屬性的Superhero.因爲超級英雄分享了通常人類許多共有的特徵(例如:name,gender),所以這應該頗有但願充分展現出子類劃分是如何工做的。
let clark = new Person( "Clark" , "Kent" );let Superhero = function( firstName, lastName , powers ){ Person.call( this, firstName, lastName ); this.powers = powers;};SuperHero.prototype = Object.create( Person.prototype );let superman = new Superhero( "Clark" ,"Kent" , ["flight","heat-vision"] );console.log( superman );
Superhero構造器建立了一個自Peroson降低的對象。這種類型的對象擁有鏈中位於它之上的對象的屬性,並且若是咱們在Person對象中設置了默認的值,Superhero可以使用特定於它的對象的值覆蓋任何繼承的值。
Mixin(織入目標類)
在Javascript中,咱們會將從Mixin繼承看做是經過擴展收集功能的一種途徑.咱們定義的每個新的對象都有一個原型,從其中它能夠繼承更多的屬性.原型能夠從其餘對象繼承而來,可是更重要的是,可以爲任意數量的對象定義屬性.咱們能夠利用這一事實來促進功能重用。
Mix容許對象以最小量的複雜性從它們那裏借用(或者說繼承)功能.做爲一種利用Javascript對象原型工做得很好的模式,它爲咱們提供了從不止一個Mix處分享功能的至關靈活,但比多繼承有效得多得多的方式。
它們能夠被看作是其屬性和方法能夠很容易的在其它大量對象原型共享的對象.想象一下咱們定義了一個在一個標準對象字面量中含有實用功能的Mixin,以下所示:
let myMixins = {
moveUp: function(){ console.log( "move up" ); },
moveDown: function(){ console.log( "move down" ); },
stop: function(){ console.log( "stop! in the name of love!" ); }
};
而後咱們能夠方便的擴展示有構造器功能的原型,使其包含這種使用一個 以下面的score.js_.extends()方法輔助器的行爲:
function carAnimator(){ this.moveLeft = function(){ console.log( "move left" ); };}function personAnimator(){ this.moveRandomly = function(){ /*..*/ };}_.extend( carAnimator.prototype, myMixins );_.extend( personAnimator.prototype, myMixins );let myAnimator = new carAnimator();myAnimator.moveLeft();myAnimator.moveDown();myAnimator.stop();
如咱們所見,這容許咱們將通用的行爲輕易的"混"入至關普通對象構造器中。
在接下來的示例中,咱們有兩個構造器:一個Car和一個Mixin.咱們將要作的是靜Car參數化(另一種說法是擴展),以便它可以繼承Mixin中的特定方法,名叫driveForwar()和driveBackward().這一次咱們不會使用Underscore.js。
取而代之,這個示例將演示如何將一個構造器參數化,以便在無需重複每個構造器函數過程的前提下包含其功能。
let Car = function ( settings ) { this.model = settings.model || "no model provided"; this.color = settings.color || "no colour provided";};// Mixinlet Mixin = function () {};Mixin.prototype = { driveForward: function () { console.log( "drive forward" ); }, driveBackward: function () { console.log( "drive backward" ); }, driveSideways: function () { console.log( "drive sideways" ); }};function augment( receivingClass, givingClass ) { if ( arguments[2] ) { for ( var i = 2, len = arguments.length; i < len; i++ ) { receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; } }else { for ( let methodName in givingClass.prototype ) { if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) { receivingClass.prototype[methodName] = givingClass.prototype[methodName]; } } }}augment( Car, Mixin, "driveForward", "driveBackward" );let myCar = new Car({ model: "Ford Escort", color: "blue"});myCar.driveForward();myCar.driveBackward();
augment( Car, Mixin );let mySportsCar = new Car({ model: "Porsche", color: "red"});mySportsCar.driveSideways();
優勢 & 缺點
Mixin支持在一個系統中降解功能的重複性,增長功能的重用性.在一些應用程序也許須要在全部的對象實體共享行爲的地方,咱們可以經過在一個Mixin中維護這個共享的功能,來很容易的避免任何重複,而所以專一於只實現咱們系統中真正彼此不一樣的功能。
也就是說,對Mixin的反作用是值得商榷的.一些開發者感受將功能注入到對象的原型中是一個壞點子,由於它會同時致使原型污染和必定程度上的對咱們原有功能的不肯定性.在大型的系統中,極可能是有這種狀況的。
可是,強大的文檔對最大限度的減小對待功能中的混入源的迷惑是有幫助的,並且對於每一種模式而言,若是在實現過程當中當心行事,咱們應該是沒多大問題的。
裝飾器是旨在提高重用性能的一種結構性設計模式。同Mixin相似,它能夠被看做是應用子類劃分的另一種有價值的可選方案。
典型的裝飾器提供了向一個系統中現有的類動態添加行爲的能力。其創意是裝飾自己並不關心類的基礎功能,而只是將它自身拷貝到超類之中。
裝飾器模式並不去深刻依賴於對象是如何建立的,而是專一於擴展它們的功能這一問題上。不一樣於只依賴於原型繼承,咱們在一個簡單的基礎對象上面逐步添加可以提供附加功能的裝飾對象。它的想法是,不一樣於子類劃分,咱們向一個基礎對象添加(裝飾)屬性或者方法,所以它會是更加輕巧的。
向Javascript中的對象添加新的屬性是一個很是直接了當的過程,所以將這一特定牢記於心,一個很是簡單的裝飾器能夠實現以下:
示例1:帶有新功能的裝飾構造器
function vehicle( vehicleType ){ this.vehicleType = vehicleType || "car"; this.model = "default"; this.license = "00000-000";}let testInstance = new vehicle( "car" );console.log( testInstance );// vehicle: car, model:default, license: 00000-000
let truck = new vehicle( "truck" );truck.setModel = function( modelName ){ this.model = modelName;};truck.setColor = function( color ){ this.color = color;};truck.setModel( "CAT" );truck.setColor( "blue" );console.log( truck );// vehicle:truck, model:CAT, color: blue
let secondInstance = new vehicle( "car" );console.log( secondInstance );// vehicle: car, model:default, license: 00000-000
示例2:帶有多個裝飾器的裝飾對象
function MacBook() { this.cost = function () { return 997; }; this.screenSize = function () { return 11.6; };}function Memory( macbook ) { let v = macbook.cost(); macbook.cost = function() { return v + 75; };}function Engraving( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 200; };}function Insurance( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 250; };}
let mb = new MacBook();Memory( mb );Engraving( mb );Insurance( mb );console.log( mb.cost() );// 1522console.log( mb.screenSize() );// 11.6
在上面的示例中,咱們的裝飾器重載了超類對象MacBook()的 object.cost()函數,使其返回的Macbook的當前價格加上了被定製後升級的價格。
這被看作是對原來的Macbook對象構造器方法的裝飾,它並無將其重寫(例如,screenSize()),咱們所定義的Macbook的其它屬性也保持不變,無缺完好。
優勢 & 缺點
由於它能夠被透明的使用,而且也至關的靈活,所以開發者都挺樂意去使用這個模式——如咱們所見,對象能夠用新的行爲封裝或者「裝飾」起來,然後繼續使用,並不用去擔憂基礎的對象被改變。在一個更加普遍的範圍內,這一模式也避免了咱們去依賴大量子類來實現一樣的效果。
然而在實現這個模式時,也存在咱們應該意識到的缺點。若是窮於管理,它也會因爲引入了許多微小可是類似的對象到咱們的命名空間中,從而顯著的使得咱們的應用程序架構變得複雜起來。這裏所擔心的是,除了漸漸變得難於管理,其餘不能熟練使用這個模式的開發者也可能會有一段要掌握它被使用的理由的艱難時期。
足夠的註釋或者對模式的研究,對此應該有助益,而只要咱們對在咱們的應程序中的多大範圍內使用這一模式有所掌控的話,咱們就能讓兩方面都獲得改善。
享元模式是一個優化重複、緩慢和低效數據共享代碼的經典結構化解決方案。它的目標是以相關對象儘量多的共享數據,來減小應用程序中內存的使用(例如:應用程序的配置、狀態等)。
此模式最早由Paul Calder 和 Mark Linton在1990提出,並用拳擊等級中少於112磅體重的等級名稱來命名。享元(「Flyweight」英語中的輕量級)的名稱自己是從以幫以助咱們完成減小重量(內存標記)爲目標的重量等級推導出的。
實際應用中,輕量級的數據共享採集被多個對象使用的類似對象或數據結構,並將這些數據放置於單個的擴展對象中。咱們能夠把它傳遞給依靠這些數據的對象,而不是在他們每一個上面都存儲一次。
使用享元
有兩種方法來使用享元。第一種是數據層,基於存儲在內存中的大量相同對象的數據共享的概念。第二種是DOM層,享元模式被做爲事件管理中心,以免將事件處理程序關聯到咱們須要相同行爲父容器的全部子節點上。享元模式一般被更多的用於數據層,咱們先來看看它。
享元和數據共享
對於這個應用程序而言,圍繞經典的享元模式有更多須要咱們意識到的概念。享元模式中有一個兩種狀態的概念——內在和外在。內在信息可能會被咱們的對象中的內部方法所須要,它們絕對不能夠做爲功能被帶出。外在信息則能夠被移除或者放在外部存儲。
帶有相同內在數據的對象能夠被一個單獨的共享對象所代替,它經過一個工廠方法被建立出來。這容許咱們去顯著下降隱式數據的存儲數量。
箇中的好處是咱們可以留心於已經被初始化的對象,讓只有不一樣於咱們已經擁有的對象的內在狀態時,新的拷貝纔會被建立。
咱們使用一個管理器來處理外在狀態。如何實現能夠有所不一樣,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態以及它們所屬的享元對象的中心數據庫。
經典的享元實現
近幾年享元模式已經在Javascript中獲得了深刻的應用,咱們會用到的許多實現方式其靈感來自於Java和C++的世界。
咱們來看下來自維基百科的針對享元模式的 Java 示例的 Javascript 實現。
在這個實現中咱們將要使用以下所列的三種類型的享元組件:
享元對應的是一個接口,經過此接口可以接受和控制外在狀態。
構造享元來實際的實際的實現接口,並存儲內在狀態。構造享元須是可以被共享的,而且具備操做外在狀態的能力。
享元工廠負責管理享元對象,而且也建立它們。它確保了咱們的享元對象是共享的,而且能夠對其做爲一組對象進行管理,這一組對象能夠在咱們須要的時候查詢其中的單個實體。若是一個對象已經在一個組裏面建立好了,那它就會返回該對象,不然它會在對象池中新建立一個,而且返回之。
這些對應於咱們實現中的以下定義:
CoffeeOrder:享元
CoffeeFlavor:構造享元
CoffeeOrderContext:輔助器
CoffeeFlavorFactory:享元工廠
testFlyweight:對咱們享元的使用
鴨式衝減的 「implements」
鴨式衝減容許咱們擴展一種語言或者解決方法的能力,而不須要變動運行時的源。因爲接下的方案須要使用一個Java關鍵字「implements」來實現接口,而在Javascript本地看不到這種方案,那就讓咱們首先來對它進行鴨式衝減。
Function.prototype.implementsFor 在一個對象構造器上面起做用,而且將接受一個父類(函數—)或者對象,而從繼承於普通的繼承(對於函數而言)或者虛擬繼承(對於對象而言)均可以。
// Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){ if ( parentClassOrObject.constructor === Function ) { // Normal Inheritance this.prototype = new parentClassOrObject(); this.prototype.constructor = this; this.prototype.parent = parentClassOrObject.prototype; } else { // Pure Virtual Inheritance this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject; } return this;};
咱們能夠經過讓一個函數明確的繼承自一個接口來彌補implements關鍵字的缺失。下面,爲了使咱們得以去分配支持一個對象的這些實現的功能,CoffeeFlavor實現了CoffeeOrder接口,而且必須包含其接口的方法。
let CoffeeOrder = { // Interfaces serveCoffee:function(context){}, getFlavor:function(){}};function CoffeeFlavor( newFlavor ){ let flavor = newFlavor; if( typeof this.getFlavor === "function" ){ this.getFlavor = function() { return flavor; }; } if( typeof this.serveCoffee === "function" ){ this.serveCoffee = function( context ) { console.log("Serving Coffee flavor "+ flavor+" to table number "+ context.getTable()); }; }}CoffeeFlavor.implementsFor( CoffeeOrder );function CoffeeOrderContext( tableNumber ) { return{ getTable: function() { return tableNumber; } };}function CoffeeFlavorFactory() { let flavors = {}, length = 0; return { getCoffeeFlavor: function (flavorName) { let flavor = flavors[flavorName]; if (flavor === undefined) { flavor = new CoffeeFlavor(flavorName); flavors[flavorName] = flavor; length++; } return flavor; }, getTotalCoffeeFlavorsMade: function () { return length; } };}function testFlyweight(){ let flavors = new CoffeeFlavor(), tables = new CoffeeOrderContext(), ordersMade = 0, flavorFactory; function takeOrders( flavorIn, table) { flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn ); tables[ordersMade++] = new CoffeeOrderContext( table ); } flavorFactory = new CoffeeFlavorFactory(); takeOrders("Cappuccino", 2); takeOrders("Cappuccino", 2); takeOrders("Frappe", 1); takeOrders("Frappe", 1); takeOrders("Xpresso", 1); takeOrders("Frappe", 897); takeOrders("Cappuccino", 97); takeOrders("Cappuccino", 97); takeOrders("Frappe", 3); takeOrders("Xpresso", 3); takeOrders("Cappuccino", 3); takeOrders("Xpresso", 96); takeOrders("Frappe", 552); takeOrders("Cappuccino", 121); takeOrders("Xpresso", 121); for (var i = 0; i < ordersMade; ++i) { flavors[i].serveCoffee(tables[i]); } console.log("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade());}
轉換代碼爲使用享元模式
接下來,讓咱們經過實現一個管理一個圖書館中全部書籍的系統來繼續觀察享元。分析得知每一本書的重要元數據以下:
ID
標題
做者
類型
總頁數
出版商ID
ISBN
咱們也將須要下面一些屬性,來跟蹤哪個成員是被借出的一本特定的書,借出它們的日期,還有預計的歸還日期。
借出日期
借出的成員
規定歸還時間
可用性
let Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){ this.id = id; this.title = title; this.author = author; this.genre = genre; this.pageCount = pageCount; this.publisherID = publisherID; this.ISBN = ISBN; this.checkoutDate = checkoutDate; this.checkoutMember = checkoutMember; this.dueReturnDate = dueReturnDate; this.availability = availability;};
Book.prototype = { getTitle: function () { return this.title; }, getAuthor: function () { return this.author; }, getISBN: function (){ return this.ISBN; }, updateCheckoutStatus: function( bookID, newStatus, checkoutDate , checkoutMember, newReturnDate ){ this.id = bookID; this.availability = newStatus; this.checkoutDate = checkoutDate; this.checkoutMember = checkoutMember; this.dueReturnDate = newReturnDate; }, extendCheckoutPeriod: function( bookID, newReturnDate ){ this.id = bookID; this.dueReturnDate = newReturnDate; }, isPastDue: function(bookID){ let currentDate = new Date(); return currentDate.getTime() > Date.parse( this.dueReturnDate ); }};
這對於最初小規模的藏書可能工做得還好,然而當圖書館擴充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,咱們會發現管理系統的運行隨着時間的推移會愈來愈慢。使用成千上萬的書籍對象可能會壓倒內存,而咱們能夠經過享元模式的提高來優化咱們的系統。
如今咱們能夠像下面這樣將咱們的數據分離成爲內在和外在的狀態:同書籍對象(標題,版權歸屬)相關的數據是內在的,而借出數據(借出成員,規定歸還日期)則被看作是外在的。這實際上意味着對於每一種書籍屬性的組合僅須要一個書籍對象。這仍然具備至關大的數量,但相比以前已經獲得大大的縮減了。
下面的書籍元數據組合的單一實體將在全部帶有一個特定標題的書籍拷貝中共享。
let Book = function ( title, author, genre, pageCount, publisherID, ISBN ) { this.title = title; this.author = author; this.genre = genre; this.pageCount = pageCount; this.publisherID = publisherID; this.ISBN = ISBN;};
如咱們所見,外在狀態已經被移除了。從圖書館借出所要作的一切都被轉移到一個管理器中,因爲對象數據如今是分段的,工廠能夠被用來作實例化。
一個基本工廠
如今讓咱們定義一個很是基本的工廠。咱們用它作的工做是,執行一個檢查來看看一本給定標題的書是否是以前已經在系統內建立過了;若是建立過了,咱們就返回它 - 若是沒有,一本新書就會被建立並保存,使得之後能夠訪問它。
這確保了爲每一條本質上惟一的數據,咱們只建立了一份單一的拷貝:
let BookFactory = (function () { let existingBooks = {}, existingBook; return { createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) { existingBook = existingBooks[ISBN]; if ( !!existingBook ) { return existingBook; } else { let book = new Book( title, author, genre, pageCount, publisherID, ISBN ); existingBooks[ISBN] = book; return book; } } };});
管理外在狀態
下一步,咱們須要將那些從Book對象中移除的狀態存儲到某一個地方——幸運的是一個管理器(咱們會將其定義成一個單例)能夠被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱做書籍借出記錄。
這些咱們的管理器都將會存儲,而且也包含咱們在對Book類進行享元優化期間剝離的同借出相關的邏輯。
let BookRecordManager = (function () { let bookRecordDatabase = {}; return { addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) { let book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN ); bookRecordDatabase[id] = { checkoutMember: checkoutMember, checkoutDate: checkoutDate, dueReturnDate: dueReturnDate, availability: availability, book: book }; }, updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) { let record = bookRecordDatabase[bookID]; record.availability = newStatus; record.checkoutDate = checkoutDate; record.checkoutMember = checkoutMember; record.dueReturnDate = newReturnDate; }, extendCheckoutPeriod: function ( bookID, newReturnDate ) { bookRecordDatabase[bookID].dueReturnDate = newReturnDate; }, isPastDue: function ( bookID ) { let currentDate = new Date(); return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate ); } };});
這些改變的結果是全部從Book類中擷取的數據如今被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與咱們之前使用大量對象相比能夠被認爲是更加高效的東西。同書籍借出相關的方法也被設置在這裏,由於它們處理的數據是外在的而不內在的。
這個過程確實給咱們最終的解決方法增長了一點點複雜性,然而同已經明智解決的數據性能問題相比,這只是一個小擔心,若是咱們有同一本書的30份拷貝,如今咱們只須要存儲它一次就夠了。
每個函數也會佔用內存。使用享元模式這些函數只在一個地方存在(就是在管理器上),而且不是在每個對象上面,這節約了內存上的使用。