你應(yīng)該知道的 4 種 JavaScript 設(shè)計模式

英文 | https://www.digitalocean.com/community/tutorial_series/javascript-design-patterns
翻譯 | 楊小愛
(function() {// declare private variables and/or functionsreturn {// declare public variables and/or functions}})();
在這里,我們在返回我們想要返回的對象之前實例化私有變量和/或函數(shù)。我們閉包之外的代碼無法訪問這些私有變量,因為它不在同一個范圍內(nèi)。讓我們看一個更具體的實現(xiàn):
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 命名空間中引用。但是,在模塊之外時,無法引用內(nèi)容。
揭示模塊模式
模塊模式的一種變體稱為顯示模塊模式。目的是維護封裝并揭示對象字面量中返回的某些變量和方法。直接實現(xiàn)如下所示:
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
盡管這看起來更簡潔,但一個明顯的缺點是無法引用私有方法。這可能會帶來單元測試挑戰(zhàn)。同樣,公共行為是不可覆蓋的。
JavaScript 中的觀察者設(shè)計模式
很多時候,當(dāng)應(yīng)用程序的一部分發(fā)生變化時,其他部分也需要更新。在 AngularJS 中,如果 $scope 對象更新,可以觸發(fā)一個事件來通知另一個組件。觀察者模式就是這樣結(jié)合的——如果一個對象被修改,它會向依賴對象廣播發(fā)生了變化。
另一個主要的例子是模型-視圖-控制器(MVC)架構(gòu)。模型更改時視圖會更新。一個好處是將視圖與模型分離以減少依賴關(guān)系。

維基百科上的觀察者設(shè)計模式
如 UML 圖中所示,必要的對象是主體、觀察者和具體對象。主題包含對具體觀察者的引用,以通知任何更改。Observer 對象是一個抽象類,它允許具體的觀察者實現(xiàn) notify 方法。
讓我們看一個通過事件管理包含觀察者模式的 AngularJS 示例。
// Controller 1$scope.$on('nameChanged', function(event, args) {$scope.name = args.name;});...// Controller 2$scope.userNameChanged = function(name) {$scope.$emit('nameChanged', {name: name});};
對于觀察者模式,區(qū)分獨立對象或主體很重要。
值得注意的是,盡管觀察者模式確實提供了許多優(yōu)點,但缺點之一是隨著觀察者數(shù)量的增加,性能會顯著下降。最臭名昭著的觀察者之一是觀察者模式。在 AngularJS 中,我們可以觀察變量、函數(shù)和對象。$$digest 循環(huán)運行并在修改范圍對象時向每個觀察者通知新值。
我們可以在 JavaScript 中創(chuàng)建自己的主題和觀察者。讓我們看看這是如何實現(xiàn)的:
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!
發(fā)布/訂閱
然而,發(fā)布/訂閱模式使用位于希望接收通知的對象(訂閱者)和觸發(fā)事件的對象(發(fā)布者)之間的主題/事件通道。此事件系統(tǒng)允許代碼定義特定于應(yīng)用程序的事件,這些事件可以傳遞包含訂閱者所需值的自定義參數(shù)。這里的想法是避免訂閱者和發(fā)布者之間的依賴關(guān)系。
這與觀察者模式不同,因為任何訂閱者都實現(xiàn)了適當(dāng)?shù)氖录幚沓绦騺碜院徒邮瞻l(fā)布者廣播的主題通知。
盡管存在區(qū)別,但許多開發(fā)人員選擇將發(fā)布/訂閱設(shè)計模式與觀察者聚合。發(fā)布/訂閱模式中的訂閱者通過某種消息傳遞媒介得到通知,但觀察者通過實現(xiàn)類似于主題的處理程序得到通知。
在 AngularJS 中,訂閱者使用 $on('event', callback) 來“訂閱”事件,而發(fā)布者使用 $emit('event', args) 或 $broadcast('event', args) 來“發(fā)布”事件.
JavaScript 中的原型設(shè)計模式
任何 JavaScript 開發(fā)人員要么看到過關(guān)鍵字原型,被原型繼承弄糊涂了,要么在他們的代碼中實現(xiàn)了原型。Prototype 設(shè)計模式依賴于 JavaScript 原型繼承。原型模型主要用于在性能密集型情況下創(chuàng)建對象。
創(chuàng)建的對象是傳遞的原始對象的克?。\克?。?。原型模式的一個用例是執(zhí)行廣泛的數(shù)據(jù)庫操作以創(chuàng)建用于應(yīng)用程序其他部分的對象。如果另一個進程需要使用這個對象,而不是必須執(zhí)行這個大量的數(shù)據(jù)庫操作,克隆先前創(chuàng)建的對象將是有利的。

維基百科上的原型設(shè)計模式
這個 UML 描述了如何使用原型接口來克隆具體的實現(xiàn)。
要克隆一個對象,必須存在一個構(gòu)造函數(shù)來實例化第一個對象。接下來,通過使用關(guān)鍵字原型變量和方法綁定到對象的結(jié)構(gòu)。
讓我們看一個基本的例子:
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}
構(gòu)造函數(shù)允許創(chuàng)建單個 TeslaModelS 對象。當(dāng)創(chuàng)建新的 TeslaModelS 對象時,它會保留在構(gòu)造函數(shù)中初始化的狀態(tài)。此外,維護函數(shù) go 和 stop 很容易,因為我們用原型聲明了它們。一種在原型上擴展函數(shù)的同義方式,如下所述:
var TeslaModelS = function() {this.numWheels = 4;this.manufacturer = 'Tesla';this.make = 'Model S';}TeslaModelS.prototype = {go: function() {// Rotate wheels},stop: function() {// Apply brake pads}}
揭示原型模式
與模塊模式類似,原型模式也有一個顯著的變化。顯示原型模式提供了對公共和私有成員的封裝,因為它返回一個對象字面量。
由于我們要返回一個對象,我們將在原型對象前面加上一個函數(shù)。通過擴展上面的示例,我們可以選擇要在當(dāng)前原型中公開的內(nèi)容以保留其訪問級別:
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}}();
請注意,由于超出了返回對象的范圍,函數(shù) stop 和 go 將如何與返回對象屏蔽。由于 JavaScript 原生支持原型繼承,因此無需重寫底層特性。
JavaScript 中的單例設(shè)計模式
Singleton 只允許單個實例化,但同一對象的多個實例。Singleton 限制客戶端創(chuàng)建多個對象,在創(chuàng)建第一個對象后,它將返回自身的實例。
對于大多數(shù)以前沒有使用過單例的人來說,很難找到單例的用例。一個例子是使用辦公室打印機。如果一個辦公室有十個人,他們都使用一臺打印機,十臺計算機共享一臺打印機(實例)。通過共享一臺打印機,他們共享相同的資源。
var printer = (function () {var printerInstance;function create () {function print() {// underlying printer mechanics}function turnOn() {// warm up// check for paper}return {// public + private states and behaviorsprint: print,turnOn: turnOn};}return {getInstance: function() {if(!printerInstance) {printerInstance = create();}return printerInstance;}};function Singleton () {if(!printerInstance) {printerInstance = intialize();}};})();
create 方法是私有的,因為我們不希望客戶端訪問它,但是請注意 getInstance 方法是公共的。每個職員可以通過與 getInstance 方法交互來生成一個打印機實例,如下所示:
var officePrinter = printer.getInstance();在 AngularJS 中,Singleton 很普遍,最值得注意的是服務(wù)、工廠和提供者。由于它們維護狀態(tài)并提供資源訪問,因此創(chuàng)建兩個實例會破壞共享服務(wù)/工廠/提供者的觀點。
當(dāng)多個線程嘗試訪問同一資源時,多線程應(yīng)用程序中會出現(xiàn)競爭條件。單例容易受到競爭條件的影響,例如,如果沒有先初始化實例,則兩個線程可以創(chuàng)建兩個對象,而不是返回和實例。這違背了單例的目的。因此,開發(fā)人員在多線程應(yīng)用程序中實現(xiàn)單例時必須了解同步。
結(jié)論
設(shè)計模式經(jīng)常用于大型應(yīng)用程序中,但要了解,在某些時候某些地方,其中一種模式可能優(yōu)于另一種的模式,需要實踐。
在構(gòu)建任何應(yīng)用程序之前,您應(yīng)該徹底考慮每個參與者以及他們?nèi)绾蜗嗷ソ换?。在回顧了模塊、原型、觀察者和單例設(shè)計模式之后,您應(yīng)該能夠識別這些模式并在項目中使用它們。
學(xué)習(xí)更多技能
請點擊下方公眾號
![]()

