2016-10-07html
每一個JS開發者都力求寫出可維護、複用性和可讀性高的代碼。隨着應用不斷擴大,代碼組織的合理性也愈來愈重要。設計模式爲特定環境下的常見問題提供了一個組織結構,對於克服這些挑戰起到相當重要的做用。nginx
JavaScript 網頁開發者在建立應用時,頻繁地跟設計模式打交道(甚至在不知情的狀況下)。數據庫
儘管特定環境下有各類各樣的設計模式,JS 開發者仍是傾向於使用一些習慣性的模式。設計模式
在這篇文章中,我將討論這些常見的設計模式,展出優化代碼庫的方法,並深刻解讀JavaScript的內部構件。多線程
本文討論的設計模式包括這幾種:閉包
模塊設計模式架構
原型模式ide
觀察者模式svg
單例模式模塊化
儘管每種模式都包含不少屬性,這裏我強調如下幾點:
上下文: 設計模式的使用場景
問題: 咱們嘗試解決的問題是什麼?
解決方法: 使用設計模式如何解決咱們提出的問題?
實施: 實施方案看起來怎樣?
JS模塊化是使用最廣泛的設計模式,用於保持特殊的代碼塊與其它組件之間互相獨立。爲支持結構良好的代碼提供了鬆耦合。
對於熟悉面向對象的開發者來講,模塊就是JS的 「類」。封裝是「類」的衆多優勢之一,能夠確保它自己的狀態和行爲不被其它的類訪問到。模塊設計模式有公有和私有兩種訪問級別(除此以外,還有比較少爲人知的保護級別、特權級別)。
考慮到私有的做用域,模塊應該是一個當即調用函數(IIFE) ,也就是說,它是一個保護其私有變量和方法的閉包。(然而,它返回的卻不是一個函數,而是一個對象)。
它的寫法就是這樣的:
(function() { // declare private variables and/or functions return { // declare public variables and/or functions } })();
咱們在返回一個對象以前,先初始化一下私有的變量和方法。因爲做用域不一樣,閉包外面的代碼是沒法訪問到閉包內的私有變量的。一塊兒來看下更具體的實現方法:
var HTMLChanger = (function() { var contents = 'contents' var changeHTML = function() { var element = document.getElementById('attribute-to-change'); element.innerHTML = contents; } return { callChangeHTML: function() { changeHTML(); console.log(contents); } }; })(); HTMLChanger.callChangeHTML(); // Outputs: 'contents' console.log(HTMLChanger.contents); // undefined
請注意 callChangeHTML
是在返回的對象中綁定的,所以能夠訪問到 HTMLChanger
這個命名空間內的變量。然而,在模塊外面,是不能訪問到閉包裏面的 contents
的。
模塊模式的另外一種變體稱爲 揭示性模塊模式,它主要是爲了在保持封裝性的同時,揭示在對象字面量中返回的特定的變量和方法。直接的實現方式相似這樣:
var Exposer = (function() { var privateVariable = 10; var privateMethod = function() { console.log('Inside a private method!'); privateVariable++; } var methodToExpose = function() { console.log('This is a method I want to expose!'); } var otherMethodIWantToExpose = function() { privateMethod(); } return { first: methodToExpose, second: otherMethodIWantToExpose }; })(); Exposer.first(); // Output: This is a method I want to expose! Exposer.second(); // Output: Inside a private method! Exposer.methodToExpose; // undefined
儘管這樣看起來更加簡潔,但它是有明顯不足的 -- 不能引用私有變量。這會給單元測試帶來必定的挑戰。相似地,公有行爲也是不可重寫的。
JS開發者要麼把 原型
和 原型繼承
相互混淆,要麼在他們的代碼裏面直接使用原型。原型設計模式依賴於JavaScript原型繼承. 原型模式主要用於爲高性能環境建立對象。
被建立的對象是從傳下來的原對象克隆(淺克隆)出來的。原型模式的一種使用場景,是執行一個擴展性的數據庫操做來建立一個對象,把該對象用於應用的其餘層面。若是其餘流程須要用到這個對象,咱們不須要大量地操做數據庫,只要克隆一下以前建立的對象就能夠了。與其實質性地操做數據庫,不如從以前建立的對象克隆一個更具優點。
UML 描述了原型交互是如何被用於克隆具體的代碼實施方案的。
要克隆一個對象,必須存在一個構造器來實例化第一個對象。接下來,經過使用 prototype 的變量和方法來綁定對象的結構。一塊兒來看下基本的示例:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype.go = function() { // Rotate wheels } TeslaModelS.prototype.stop = function() { // Apply brake pads }
構造器 TeslaModelS
容許建立一個簡單的 TeslaModelS 對象。對於一個新建立的 TeslaModelS 對象,它將保持構造器初始化的狀態。此外,它也很簡單的持有 go 和 stop 這兩個方法,由於這兩個方法是在 prototype 聲明的。在原型上拓展方法,還能夠這樣寫:
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } }
相似於模塊模式,原型模式也有一個 揭示性模式
。揭示性原型模式
經過返回一個對象字面量,對公有和私有的成員進行封裝。
因爲咱們返回的是一個對象,咱們將在原型對象上添加 function
的前綴。經過對以上例子進行改寫,咱們能夠選擇在當前的 prototype
暴露哪些方法或變量,以此來保護它們的訪問層級。
var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = 'Tesla'; this.make = 'Model S'; } TeslaModelS.prototype = function() { var go = function() { // Rotate wheels }; var stop = function() { // Apply brake pads }; return { pressBrakePedal: stop, pressGasPedal: go } }();
請注意 stop
和 go
兩個方法是被隔開的,由於他們在所返回的對象做用域以外。因爲 JavaScript 自己支持原型繼承,也就不必重寫基本的功能了。
不少時候,當應用的一部分改變了,另外一部分也須要相應更新。在 AngularJs 裏面,若是 $scope
被更新,就會觸發一個事件去通知其餘組件。結合觀察這模式就是:若是一個對象改變了,它只要派發 broadcasts 事件通知依賴的對象它已經改變了則可。
又一個典型的例子就是 model-view-controller (MVC)
架構了;當 model
改變時, 更新相應的 view
。這樣作有一個好處,就是從 model
上解耦出 view
來減小依賴。
 { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit('nameChanged', {name: name}); };
使用觀察者模式,重要的一點就是要區分獨立的對象或者 subject(主體)。
在看到觀察者模式衆多優勢的同時,咱們必須注意到它的一個缺點:隨着觀察者數量的增長,應用的性能會大大下降。你們都比較熟悉的觀察者就是 watchers 。 在AngularJS中,咱們能夠 watch 變量、方法和對象。$digest 循環更新,當一個做用域內對象被修改時,它就把新的值告訴每一個監聽者。
咱們能夠在JS中建立本身的主體和觀察者。一塊兒來看下下面的代碼是如何運行的:
var Subject = function() { this.observers = []; return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers[index].notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } }; }; var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } } } var subject = new Subject(); var observer1 = new Observer(); var observer2 = new Observer(); var observer3 = new Observer(); var observer4 = new Observer(); subject.subscribeObserver(observer1); subject.subscribeObserver(observer2); subject.subscribeObserver(observer3); subject.subscribeObserver(observer4); subject.notifyObserver(observer2); // Observer 2 is notified! subject.notifyAllObservers(); // Observer 1 is notified! // Observer 2 is notified! // Observer 3 is notified! // Observer 4 is notified!
然而,發佈、訂閱模式是採用一個話題來綁定發佈者和訂閱者之間的關係,訂閱者接收事件通知,發佈者派發事件。該事件系統支持定義特殊應用的事件,能夠傳遞包含訂閱者自己須要的自定義參數。這樣作主要是爲了不訂閱者和發佈者之間的依賴。
這裏有別於觀察者模式的是,任何訂閱者均可以經過恰當的事件處理器來註冊並接受發佈者廣播的通知。
不少開發者選擇把 發佈訂閱模式
和 觀察者模式
結合起來用,儘管他們最終的目標只有一個。發佈訂閱模式中的訂閱者是經過一些通信媒介被告知的,而觀察者則是經過執行事件處理器來得到消息通知。
在 AngularJs, 訂閱者使用 $on(event、cbk)
來訂閱一個事件,發佈者則使用$emit(‘event’, args)
或者 $broadcast(‘event’, args)
來發佈一個事件。
單例模式只容許實例化一個對象,可是相同的對象,會用不少個實例。單例模式制約着客戶端建立多個對象。第一個對象建立後,就返回實例自己。
單例模式比較少用,很難找到實際開發的例子。使用一個辦公室打印機的例子吧。假設辦公室有10我的,他們都用到打印機,10臺電腦共享一部打印機(一個實例)。經過分享一部打印機,他們共享相同的資源。
var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })();
create
這個方法是私有的,由於咱們不但願它被外部人員訪問到,然而,getInstance
方法是公有的。每一個辦公人員均可以實例化一個 printer
,只須要這樣調用一下:
`var officePrinter = printer.getInstance();`
單例模式在 AngularJS 至關流行,最多見的是做爲 services
、factories
、和 providers
。它們維護狀態,提供資源訪問,建立兩個實例擺脫一個共享的service/factory/provider
。
在多線程的應用中,當多個線程嘗試去訪問同個資源時,就會出現 競爭狀態
。單例模式會受到競爭狀態的干擾,好比在沒有初始化實例的狀況下,兩個線程會建立兩個對象,而不是返回一個實例。這與單例模式的目的是相悖的。所以,開發者在多線程應用裏面使用單例模式時,必須清楚同步性。
設計模式常常用於比較大型的應用,想知道哪一種模式更具優點,來實踐吧。
在構建任何應用以前,都應該全面地考慮每一個角色,以及它們之間存在的關係。在回顧 模塊模式
、原型模式
、觀察者模式
和 單例模式
以後,你應該可以區分它們,而且在實際開發中使用它們了。