Every developer strives to write maintainable, readable, and reusable code.
很多開(kāi)發(fā)人員在學(xué)習(xí)設(shè)計(jì)模式的時(shí)候都特別的 被動(dòng),為了學(xué)習(xí)設(shè)計(jì)模式而學(xué)習(xí)骄噪,學(xué)完就忘記很少實(shí)踐在工作中岔乔,因此在談JavaScript設(shè)計(jì)模式之前挠日,首先我們要明確以下幾個(gè)問(wèn)題:
- Context: 什么情況下使用設(shè)計(jì)模式?
- Problem: 我們要解決的問(wèn)題是什么?
- Solution: 設(shè)計(jì)模式是怎么解決這個(gè)問(wèn)題的?
- Implementation: 要怎么實(shí)現(xiàn)?
帶著這些問(wèn)題,本文介紹了四種JavaScript開(kāi)發(fā)人員必知必會(huì)的設(shè)計(jì)模式治唤。
Module Design Pattern
模塊設(shè)計(jì)模式與 面向?qū)ο?object-oriented) 的語(yǔ)言非常相似贞间,可以將一個(gè)模塊看作是一個(gè)class, 其中優(yōu)點(diǎn)之一就是 封裝的特性粹懒,可以提高我們代碼的可維護(hù)性和易讀性,在模塊的外部無(wú)法操作模塊內(nèi)部的變量顷级,只能通過(guò)模塊提供的接口來(lái)訪(fǎng)問(wèn)凫乖。該設(shè)計(jì)模式是JavaScript最常見(jiàn)的設(shè)計(jì)模式之一,下面我們來(lái)實(shí)現(xiàn)該設(shè)計(jì)模式弓颈。
在JavaScript中實(shí)現(xiàn)“封裝”帽芽,我們可以用立即執(zhí)行函數(shù)(IIFE)創(chuàng)建一個(gè)閉包的結(jié)構(gòu)
(function() {
// declare private variables and/or functions
return {
// declare public variables and/or functions
}
})();
下面讓我們來(lái)完整這個(gè)設(shè)計(jì)模式。
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
上面的代碼我們定一個(gè)來(lái)一個(gè)模塊Exposer
, 我們可以通過(guò)該模塊提供的接口first
和 second
來(lái)操作這個(gè)模塊而無(wú)需關(guān)心其內(nèi)部實(shí)現(xiàn)翔冀,當(dāng)試圖直接訪(fǎng)問(wèn)其內(nèi)部變量的時(shí)候則是無(wú)效的嚣镜,得到的值是undefined
。很多JavaScript庫(kù)中都可以看到該設(shè)計(jì)模式的影子橘蜜。
Singleton Design Pattern
單例模式,顧名思義只有一個(gè)實(shí)例付呕。該設(shè)計(jì)模式的用途也非常的廣泛并且實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單计福。比如現(xiàn)在有一個(gè)辦公室,有五名員工公用一臺(tái)打印機(jī)徽职,那么我們就可以使用單例模式來(lái)處理這種情況象颖。
根據(jù)前面討論的模塊設(shè)計(jì)模式,我們先創(chuàng)建一個(gè)打印機(jī)模塊printer
姆钉,該模塊提供一個(gè)外部接口讓五名使用者可以訪(fǎng)問(wèn)這臺(tái)打印機(jī)说订。為了確保這五個(gè)人用的是同一臺(tái)打印機(jī),單例模式實(shí)現(xiàn)的關(guān)鍵是: 當(dāng)有人第一次訪(fǎng)問(wèn)實(shí)例時(shí)創(chuàng)建打印機(jī)實(shí)例printerInstance
潮瓶,并且保存起來(lái)陶冷。當(dāng)其他人再次訪(fǎng)問(wèn)該實(shí)例時(shí)直接返回這個(gè)保存起來(lái)的實(shí)例。
var printer = (function () {
var printerInstance;
function create() {
return {
turnOn: function turnOn() {
console.log('working')
}
}
}
return {
getInstance: function () {
if (!printerInstance) {
printerInstance = create();
}
return printerInstance;
}
}
})()
printer.getInstance().turnOn() // output: working
從上面代碼我們可以看到printer
模塊提供了一個(gè)唯一外部可以訪(fǎng)問(wèn)的接口getInstance
毯辅,當(dāng)?shù)谝淮卧L(fǎng)問(wèn)該接口時(shí)埂伦,我們先判斷實(shí)例是否被創(chuàng)建,如果沒(méi)有創(chuàng)建則使用create()
創(chuàng)建思恐,如果已經(jīng)創(chuàng)建則返回唯一的實(shí)例printerInstance
沾谜。大家可以使用單例模式設(shè)計(jì)一個(gè)計(jì)數(shù)器練練手膊毁,定義一個(gè)計(jì)數(shù)器模塊,提供增加基跑、減少婚温、查看三個(gè)接口。
Observer Design Pattern
觀察者模式媳否,當(dāng)應(yīng)用的一個(gè)部分變動(dòng)時(shí)栅螟,其他部分也會(huì)更新。比如Model-View-Controller(MVC)架構(gòu)逆日,當(dāng)model變化的時(shí)候views也會(huì)更新嵌巷。在流行的Web前端Vue、Angular中也是通過(guò)觀察者模式來(lái)通知狀態(tài)變化的室抽。
實(shí)現(xiàn)一個(gè)觀察者模式至少要包含2個(gè)角色如下圖UML圖中所示: Subject
和Observer
對(duì)象 搪哪。
下面我們使用JavaScript來(lái)實(shí)現(xiàn)上圖的觀察者模式。
首先我們要定義一個(gè)Subject
對(duì)象坪圾,包含以下方法:
- 注冊(cè)觀察者對(duì)象
- 卸載觀察者對(duì)象
- 通知觀察者對(duì)象
然后我們需要定義一個(gè)觀察者對(duì)象晓折,并且實(shí)現(xiàn)通知notify
方法。也就是實(shí)現(xiàn)當(dāng)Subject
對(duì)象通知觀察者對(duì)象時(shí)兽泄,觀察者對(duì)象要做什么漓概?
var Observer = function() {
return {
notify: function(index) {
console.log("Observer " + index + " is notified!");
}
}
}
下面讓我們實(shí)現(xiàn)完整
var Subject = function() {
var observers = [];
return {
subscribeObserver: function(observer) {
observers.push(observer);
},
unsubscribeObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers.splice(index, 1);
}
},
notifyObserver: function(observer) {
var index = observers.indexOf(observer);
if(index > -1) {
observers[index].notify(index);
}
},
notifyAllObservers: function() {
for(var i = 0; i < observers.length; i++){
observers[i].notify(i);
};
}
};
};
var Observer = function() {
return {
notify: function(index) {
console.log("Observer " + index + " is notified!");
}
}
}
上面的代碼我們實(shí)現(xiàn)了Subject
對(duì)象,在其內(nèi)部聲明了一個(gè)observers
數(shù)組用來(lái)存儲(chǔ)注冊(cè)的observer
對(duì)象病梢。下面讓我們來(lái)使用這兩個(gè)對(duì)象
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!
以上我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的設(shè)計(jì)者模式胃珍,先使用subjectsubscribeObserver(observer)
注冊(cè)以后要通知的觀察者對(duì)象,當(dāng)我們想通知注冊(cè)好的觀察者對(duì)象時(shí)蜓陌,只需要使用subject.notifyObserver(observer)
即可
Constructor Pattern
在典型的面向?qū)ο笳Z(yǔ)言中觅彰,constructor 是一個(gè)特殊的初始化方法,在JavaScript中幾乎任何東西都是一個(gè)對(duì)象钮热,我們同樣也可以在JavaScript中使用constructor pattern 來(lái)初始化對(duì)象填抬。
不使用constructor pattern時(shí),我們通常是這樣初始化一個(gè)對(duì)象的:
var Pastry = {
// initialize the pastry
init: function (type, flavor, levels, price, occasion) {
this.type = type;
this.flavor = flavor;
this.levels = levels;
this.price = price;
this.occasion = occasion;
}
}
var cake = Object.create(Pastry);
cake.init("cake", "vanilla", 3, "$10", "birthday");
我們先在對(duì)象內(nèi)部添加一個(gè)init
方法隧期,然后在初始化時(shí)我們使用Object.create(object)
創(chuàng)建這個(gè)對(duì)象并調(diào)用其init
方法飒责,下面讓我們像面向?qū)ο笳Z(yǔ)言一樣使用new
關(guān)鍵字初始化一個(gè)對(duì)象通過(guò) constructor pattern。
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);
civic.toString();
上面代碼我們用constructor pattern
初始化了2個(gè)對(duì)象:civic
和mondeo
仆潮,我們首先定義了一個(gè)函數(shù)Car
并在其內(nèi)部使用關(guān)鍵字this
設(shè)置了該對(duì)象所需要的參數(shù)宏蛉,此外我們還通過(guò)在Car
的prototype
上添加toString
來(lái)擴(kuò)展該對(duì)象。
Mixin Pattern
Mixin Pattern 實(shí)現(xiàn)了面向?qū)ο笾械?繼承, 繼承的好處可以提高代碼的復(fù)用性鸵闪,比如我們現(xiàn)在有以下一個(gè)對(duì)象
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
var max = new Person('max', 'lee');
當(dāng)我們想創(chuàng)建一個(gè)新的對(duì)象檐晕,這個(gè)對(duì)象比Person
對(duì)象多了一個(gè)power
的屬性
function Superman(firstname, lastname, power){
this.firstname = firstname;
this.lastname = lastname;
this.power = power;
}
var max = new Superman('max', 'lee', 'fly');
我們可以看到 Person
和 Superman
對(duì)象基本完全一樣,只是Superman
多了一個(gè)power
屬性, 這樣代碼重復(fù)率很高辟灰,為了解決這個(gè)問(wèn)題个榕,我們可以使用 mixmin pattern 實(shí)現(xiàn)繼承來(lái)避免過(guò)多的重復(fù)代碼。
實(shí)現(xiàn) mixmin pattern有2個(gè)關(guān)鍵點(diǎn)
- 調(diào)用superclass的constructor使用
.call
- 將superclass的prototype賦值給sub-class通過(guò)
Object.create(superclass.prototype)
具體實(shí)現(xiàn)如下:
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
function Superman(firstname, lastname, power){
Person.call(this, firstname, lastname);
this.power = power;
}
Superman.prototype = Object.create(Person.prototype);
var max = new Superman('max', 'lee', 'fly');
Facade Pattern
Facade Pattern是為了讓接口更加簡(jiǎn)單易用的一種設(shè)計(jì)模式, 比如jQuery用起來(lái)就是比原生的方法操作DOM更便捷芥喇。該設(shè)計(jì)模式通常和module pattern 結(jié)合使用西采。
讓我們實(shí)現(xiàn)一個(gè)$
接口來(lái)更便捷的操作DOM
var $ = (function () {
return {
query: function(element){
return document.querySelector(element);
}
}
})();
var titleElement = $.query('title'); // <title> title here </title>
Command Pattern
命令模式是把actions封裝成對(duì)象(command objects),比如我們把計(jì)算器的加減乘除這些action封裝在object里面
var Calculator = {
// addition function
add: function (num1, num2) {
return num1 + num2;
},
// subtraction function
substract: function (num1, num2) {
return num1 - num2;
},
// multiplication function
multiply: function (num1, num2) {
return num1 * num2;
},
// division function
divide: function (num1, num2) {
return num1 / num2;
},
};
我們可以像Calculator.add(1, 1)
這樣來(lái)計(jì)算加法械馆,但是我們不想增加對(duì)象之間的依賴(lài)性,以后如果Calculator
里面這些方法改變的時(shí)候武通,那么調(diào)用它的地方也都需要改動(dòng)霹崎。那么我們就可以不直接操作Calculator
而是通過(guò)“命令”來(lái)操作加減乘除。
先定義一個(gè)計(jì)算接口來(lái)接收命令
Calculator.execute = function(command){
return Calculator[command.type](command.num1, command.num2)
}
然后我們就可以這樣把命令當(dāng)作一個(gè)參數(shù)伴隨著要計(jì)算的數(shù)字傳給calc
方法
console.log(Calculator.execute({type: "divide" ,num1:1,num2:6}));
console.log(Calculator.execute({type: "multiply" ,num1:3,num2:5}));
這樣以后就算Calculator
的內(nèi)部方法以后改變了冶忱,但是調(diào)用提供的接口不需要改變尾菇。