設計模式手記(16種)

語言: CN / TW / HK

入行幾年,可能發現除了釋出訂閱者模式和單例模式知道些外,其他的模式只是對其名稱有所耳聞,只知其名,不知其義

好像這絲毫不影響我搬磚,那麼學習總結設計模式的意義在哪裡?

就好比,蓋了個新房,十年八年的屋頂也不會漏雨。但是,萬一哪一天,有塊瓦破了,需要去屋頂換瓦,那麼這個時候,梯子沒有,瓦片沒有。看著滴滴答答的雨水,是不是莫名的心酸。這個時候,就想,有梯子有瓦片該多好。

學習設計模式,就是在晴天準備梯子和瓦片,用不到則已,萬一用到了,我們就可以踩著梯子換瓦片。

那麼該如何學習設計模式呢?

幾年前遇到了某個問題,現在想起,可能具體程式碼實現是忘記了。但是解決問題的思路,大概還是能七七八八的記起來的。

所以,我認為學習設計模式的思想更為重要,具體的實現,每個設計模式可能不止一種實現方式。比如狀態模式,我們可以通過修改物件狀態進行狀態切換,但是,也可以利用陣列天然的有序性進行具體行為的組裝。

本文一萬三千字元,閱讀比較耗時,可以在右側點選目錄進入具體的設計模式。下面開始正文...

一、定義

是軟體設計過程中針對特定問題簡潔優雅的解決方案。

是經過大量實際專案在很長時間中驗證得到的最佳實踐。

是軟體開發前輩在成功和失敗中總結的智慧傳承。

二、比喻

聽到莊周夢蝶的成語,就能能感受到莊周和蝴蝶不分你我的優美意境。

聽到庖丁解牛的成語,我們會為庖丁嫻熟的技能由衷的讚歎。

聽到海市蜃樓的成語,就知道是一個虛無縹緲的事物。

當聽到單例模式,就知道該模式指的是全域性只建立一個例項。

當聽到釋出訂閱模式,就知道是用來解決事件釋出者和事件監聽者之間時間解耦關係。

當聽到中介者模式,就知道是用來解耦多個物件之間錯綜複雜的互動關係。

所以,成語之於成語背後的故事。就如,模式之於背後的解決方案。

三、使用時機

如果設計模式使用不合時宜,會出現似是而非、張冠李戴的情況。

如果能深刻的理解各個設計模式的原理,那麼就相當於掌握了大用小用之辯,方可遊刃有餘

四、使用原則

區分可變與不可變的部分,並將變化的地方進行封裝。

五、區分方式

設計模式區分不在於其實現方式,而在於其解決的問題場景。

六、按照結構分類

image.png

七、圖示和簡單案例說明

(一)建立型

1、原型模式

我們知道JavaScript中幾乎所有的資料都是物件,即使不是,通過包裝類NumberBooleanString進行轉換。

而且,物件的產生可以通過new 建構函式的方式產生。那麼,建構函式可以通過屬性prototype的屬性,為其定義屬性或者方法。

通過new可以產生多個物件,每個物件都可以訪問建構函式prototype上的屬性和方法。

這種模式就可以稱為原型模式,因為是通過建構函式建立例項物件,原型模式屬於建立型模式。

可以參照如下流程圖:

image.png

舉個簡單的例子: ``` var Person = function (name, hobby) { this.name = name; this.hobby = hobby; } Person.prototype.basicInfo = { 'address': '北京市 海淀區', 'school': '某某大學', 'major': '計算機', }

Person.prototype.sayHi = function () { console.log('大家好,我是' + this.name, '我的愛好是' + this.hobby); }

var perosn1 = new Person('張三', '踢足球') perosn1.sayHi(); console.log(perosn1.basicInfo);

var person2 = new Person('李四', '玩遊戲') person2.sayHi(); console.log(perosn1.basicInfo);

var person3 = new Person('王五', '下象棋') person3.sayHi(); console.log(perosn1.basicInfo);

var person4 = new Person('趙六', '看電影') person4.sayHi(); console.log(perosn1.basicInfo); `` 當前例子中,建構函式是Person,原型上定義了公共的基本資訊basicInfo和方法sayHi。通過new的方式,建立了person1person2person3person4。每個物件都可以執行方法sayHi,也可以列印公共屬性basicInfo`。

2、單例模式

一個環境中有且只有一個例項,並且當前環境可以訪問到它。

往小了說,當前環境可以是一個函式作用域、塊級作用域。往大了說可以是全域性window或者global環境。

image.png

舉個簡單的例子: ``` // 定義可以表示單例模式函式 var SingleInstanceMode = function (fn) { var instance; return function () { return instance || (instance = fn.call(this, arguments)) } } // 定義產生例項的目標函式 var targetFn = function () { return { info: '這是唯一一個例項' } } // 將目標函式傳入單例模式中 var createSingleInstance = SingleInstanceMode(targetFn)

// 建立例項1 var instance1 = createSingleInstance(); // 建立例項2 var instance2 = createSingleInstance(); // 判斷兩例項是否相等 console.log(instance1 === instance2) // true: 表示全域性只有唯一一個例項 `` 當前例子中首先定義可表示單例模式的函式,函式中通過閉包的方式鎖定變數instance`,在執行建立單例的函式時,如果環境中已經存在一個例項則直接返回,否則才去進行例項的建立。這就是單例模式的一種實現方式。

3、工廠模式

工廠模式指的是,批量建立物件的時候可以避免使用new + 建構函式的方式去暴露建立物件的行為,而是通過工廠模式將建立物件的行為隱藏到工廠函式內部,不僅可以批量的生產物件,而且還可以通過傳入引數改變產出產品的形態。

畫個工廠簡單的流程圖如下:

image.png

這裡我們不需要關注車間一、二、三和四具體是咋操作的,只需要關係工廠入口我們輸入的引數,和工廠出口產出的產品,舉個例子如下: ``` // 定義可以生產服裝的工廠 var factory = function (type, height) { // 定義襯衫、短褲和皮夾克的車間 var workshop = { 1: function (height) { var obj = new Object() obj.name = '襯衫' obj.height = height; return obj; }, 2: function (height) { var obj = new Object() obj.name = '短褲' obj.height = height; return obj; }, 3: function (height) { var obj = new Object() obj.name = '皮夾克' obj.height = height; return obj; }, 4: function (height) { var obj = new Object() obj.name = '西裝' obj.height = height; return obj; }, } // 不同的車間進行不同的服裝生成 return workshoptype } // 每個車間先生成一件衣服試試機器 var shirt1 = factory(1, '175cm') var shorts1 = factory(2, '178cm') var jacket1 = factory(3, '180cm') var suit1 = factory(4, '185cm')

// 機器試著沒問題,再批量生成一批襯衫 var shirt1 = factory(1, '172cm') var shirt2 = factory(1, '173cm') var shirt3 = factory(1, '174cm') var shirt4 = factory(1, '175cm') var shirt5 = factory(1, '176cm') var shirt6 = factory(1, '177cm') var shirt7 = factory(1, '178cm') var shirt8 = factory(1, '179cm') `` 當前例子中,我們不必知道工廠內部各個車間的具體情況,只需要知道服裝編號1代表襯衫2代表短褲3代表皮夾克4代表西裝`,然後再告訴工廠穿衣者的身高,即可批量生成出一批襯衫。

當然我們也可以根據實際需求對工廠內部進行改造,改造成為我們需要的工廠。

(二)結構型

4、裝飾者模式

先舉個例子作為引子,假如小帥剛造了一個手機。

image.png

小帥看著不夠帥,於是加了個帶有“帥”字的吊墜。

image.png

其實這個吊墜有沒有,手機的功能絲毫不受影響。有了吊墜,小帥覺得會手機看起來更“帥”一點點,手機還是那個手機,人機互動視窗還是視窗觸控式螢幕,並沒有變成古老的按鍵方式。(人機互動的螢幕即是人和手機互動的介面)

裝飾者模式以其不改變原物件,並且與原物件有著相同介面的特點,廣泛應用於日常開發和主流框架的功能中。

假如我們開發了一個移動端網頁,有圖書搜尋、小遊戲、音訊播放和影片播放等主要功能,初期,我們並不知道這幾個功能使用者的使用規律。

有一天,產品經理說,我想要各個功能使用者的使用規律,並且通過echarts繪製折線圖和柱狀圖,能加嗎?

這就加......

起初:

```

通過裝飾者模式增加資料埋點之後:

```

定義Function.prototype.after函式,其中通過閉包的方式快取selfFn,然後返回一個函式,該函式首先執行selfFn,再執行afterFn,這裡也很清晰的可以看出兩個函式的執行順序。

在當前例子中,首先執行進入小遊戲的功能,然後,再執行資料埋點的功能。

可以看出,加了資料埋點,執行函式是enterSmallGame,不加也是。同時,也未對原始函式enterSmallGame內部進行修改。

5、代理模式

先舉個例子作為引子,我們的本體是計算器,每天會進行大量的計算。

image.png

我們發現也會有不少重複的計算,我們引入一個代理。

image.png

圖示中,訪問代理進行資料的計算,如果是重複的計算,快取代理直接返回結果。如果是首次計算,快取代理將其傳遞給本體進行計算。

當本體處於保護、快取、虛擬或者過濾等情況下時,一個數據不適合被訪問或者一個方法不能被直接呼叫,可以採用代理模式,先建立一個代理(本體物件或者方法的替身),作為訪問者和本體之間的中介或者橋樑。

再通過程式碼對比只是用本體進行計算,和使用代理方式進行計算的異同。

// 本體計算乘積 var mult = function(){ var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; // 代理計算乘積 var proxyMult = (function(){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = mult.apply( this, arguments ); } })(); 以上是本體和代理,都可以通過傳遞引數計算乘積,有以下兩種訪問方式: - 本體計算乘積 console.log(mult( 1, 2, 3, 4 )); // 24 計算會得出24的乘積,如果再次計算會再次進行計算,如果資料量比較大的話,會重複計算; - 代理計算乘積 console.log(proxyMult( 1, 2, 3, 4 )); // 24 第一次計算會計算出24的乘積,並將其存到cache中,如cache["1,2,3,4"] = 24,第二次計算的時候,發現cache中有鍵為"1,2,3,4"的乘積,無需重複計算,直接返回。

6、介面卡模式

先舉個兩實體不匹配例子:

image.png

假如這兩塊要契合在一起,怎麼辦?

咱們先給A實體造個介面卡,如下:

image.png

再把A實體往右推一下:

image.png

通過介面卡,咱們就把A實體和B實體結合到了一起了。

介面卡模式是用來解決兩個軟體實體之間不相容的問題的設計模式。可以在不改變實體內部結構的情況下,在其中一個實體外層包裝一個介面卡,再去將兩個實體進行配合使用。

再看介面卡在程式碼中的例子:有個實體A,需要將實體A傳入實體B中,實體B返回其name對應的資料,包含名稱、地址和年齡。

// 實體A var instanceA = [{ name: '張三', address: '北京', age: 20, }, { name: '李四', address: '天津', age: 25, }, { name: '王五', address: '河北', age: 30, } ] // 實體B var instanceB = function (data, name) { return data[name] } // 實體A在實體B中進行呼叫 console.log(instanceB(instanceA, '張三')) // undefined

這裡先定義實體A作為資料,定義實體B作為呼叫函式,將實體A放入實體B中,我們執行可以發現返回的是undefined

此時,我們定義一個介面卡。

var dataAdapter = function (arr) { return arr.reduce((accumulator, currentValue) => { accumulator[currentValue['name']] = currentValue return accumulator }, {}) } 通過介面卡,將陣列物件轉換成name作為key{name:xxx, address:xxx, age:xxx}作為value的物件。

然後,將實體A進行介面卡的處理,再塞入到實體B中。 console.log(instanceB(dataAdapter(instanceA), '張三')) // {"name": "張三", "address": "北京", "age": 20} 這樣,通過介面卡dataAdapter,就可以將實體A在實體B進行使用,實現了兩個不同實體之間不相容的問題。

7、組合模式

搬了一天磚後,拖著疲憊的身體回家,開啟家門...

image.png

從圖中可以看出,開烤箱後的組合行為是烤麵包和烤腸。

進廚房後的組合行為,是開烤箱後的組合行為、煮雞蛋和煮咖啡共同組成。

回家後的行為,是由關門、開燈、進廚房的組合行為和開啟電視共同組成。

這樣,就可以組成一顆行為樹,但是,可以發現,藍色區域是多個行為的組合,而非行為的自身執行,真正的執行由具體的行為動作完成。

我們先定義一個可以返回組合行為物件新增和執行的函式: var JointCommand = function () { return { commandList: [], add: function (command) { this.commandList.push(command) }, execute: function () { for (let i = 0, command; command = this.commandList[i++];) { command.execute(); } } } } JointCommand執行後返回的物件表示組合物件,commandList表示命令或者行為的組合,add用來為組合命令新增命令,execute表示組合物件的執行,本質上是呼叫組合命令列表中的command.execute()

其中,command也可能是組合物件,執行組合物件的時候,該列表層後續的command暫時不執行。會以深度遍歷的方式,先執行組合物件的列表命令。依次類推,最終的葉子物件執行完後將執行權交給父級,層層向上,最終會完成整棵樹的命令執行。

下面我們先從整棵樹的葉子物件開始進行分析。

(1)開烤箱後的行為

// 烤麵包和烤腸 var cookieToattCommand = { execute: function () { console.log('烤麵包') } } var roastSausageCommand = { execute: function () { console.log('烤香腸') } } // 開烤箱後的組合行為 afterOpenOvenCommand = JointCommand(); afterOpenOvenCommand.add(cookieToattCommand); afterOpenOvenCommand.add(roastSausageCommand); 定義烤麵包和烤腸的單個物件,包含execute方法。

再執行JointCommand返回開啟烤箱組合物件。

最後通過afterOpenOvenCommand.add(cookieToattCommand)afterOpenOvenCommand.add(roastSausageCommand)的方式為開啟烤箱後的組合物件新增烤麵包和烤香腸命令。

(2)進入廚房後的行為 ``` var boiledEggCommand = { execute: function () { console.log('煮雞蛋') } } var makeCoffeeCommand = { execute: function () { console.log('煮咖啡') } } // 進入廚房後的組合行為 afterEnterKitchenCommand = JointCommand();

afterEnterKitchenCommand.add(afterOpenOvenCommand); afterEnterKitchenCommand.add(boiledEggCommand); afterEnterKitchenCommand.add(makeCoffeeCommand); `` 定義煮雞蛋和煮咖啡的單個物件,包含execute`方法。

再執行JointCommand返回進入廚房組合物件。

和開啟烤箱不同的地方在於,通過afterEnterKitchenCommand.add(afterOpenOvenCommand)的方式為commandList新增的是開啟烤箱後的組合物件。

最後通過afterEnterKitchenCommand.add(boiledEggCommand)afterEnterKitchenCommand.add(makeCoffeeCommand)的方式為進入廚房後的組合物件新增煮雞蛋和煮咖啡命令。

(3)回家後的行為 var closeDoorCommand = { execute: function () { console.log('關門') } } var turnOnLightCommand = { execute: function () { console.log('開燈') } } var turnOnTvCommand = { execute: function () { console.log('開啟電視') } } // 回家後的組合行為 afterGoHomeCommand = JointCommand(); afterGoHomeCommand.add(closeDoorCommand); afterGoHomeCommand.add(turnOnLightCommand); afterGoHomeCommand.add(afterEnterKitchenCommand); afterGoHomeCommand.add(turnOnTvCommand);

定義關門、開燈和開啟電視的單個物件,包含execute方法。

再執行JointCommand返回回家後的組合物件。

最後通過afterGoHomeCommand.add(closeDoorCommand)afterGoHomeCommand.add(turnOnLightCommand)afterGoHomeCommand.add(afterEnterKitchenCommand)afterEnterKitchenCommand.add(turnOnTvCommand)的方式為回家後的組合物件新增關門開燈進入廚房組合物件開啟電視

這裡需要注意,進入廚房後的組合物件已經是一棵樹了,是回家後組合物件的子樹。

當執行afterGoHomeCommand.execute()後的執行結果是:

image.png

深度遍歷流程圖如下:

image.png

小結

組合模式,在執行根組合物件、節點組合物件和葉子物件時都是execute,也就說不管從哪裡開始,都可以執行execute,這讓組合模式的使用變得簡單。訪問者幾乎沒有上手成本。簡單得像摁個按鈕,只需要知道開燈、開電視、開烤箱、咖啡機等裝置的開關一樣。

可以通過afterEnterKitchenCommand.execute()進行進入廚房後的組合行為執行。

可以通過afterOpenOvenCommand.execute()進行開啟烤箱後的組合行為執行。

也可以通過turnOnLightCommand.execute()turnOnTvCommand.execute()進行葉子物件的執行,分別執行了開燈和開啟電視的命令。

最後抽象組合模式的樹形結構:

image.png

組合模式表示的是區域性和整體的關係,區域性和整體的關係往往是通過樹來描述的。樹我們知道由根、枝幹和葉子構成,同樣,抽象的樹也是由根節點,節點和葉子節點構成。

8、享元模式

享元模式是一種用於效能優化的模式,其主要方式是通過運用共享技術來實現複雜物件總量的減少。將結構整體合理劃分內部狀態和外部狀態,內部狀態是那種不變化的,穩定的,也可以稱之為享元,外部狀態是那種變化的,不定的。

先舉個例子:餐館,如果使用一次性筷子,就餐次數增加一次,就需要多一雙筷子。

image.png

那麼,假如每天500個就餐次數,一年就是500 × 365 = 182500

用程式碼實現一次性筷子的建立和銷燬情況: // 筷子序列號 var serialNumber = 1; // 筷子建構函式 var Chopsticks = function (serialNumber) { this.serialNumber = serialNumber; } // 筷子管理 var chopsticksManager = (function () { var outerData = [] return { add: function (count) { for (var i = 0; i < count; i++) { outerData.push(new Chopsticks(serialNumber++)) } return outerData; }, destroy: function () { while (outerData.length) { outerData.pop(); } }, } })() // 一次性筷子的建立 var chopsticksArr = chopsticksManager.add(182500) // 一次性筷子的銷燬 chopsticksManager.destroy() 在當前例子中,我們定義了筷子建構函式,然後通過chopsticksManager進行筷子的管理。我們不考慮筷子分批次建立和分批次銷燬的情況,我們彙總成一次進行處理。通過chopsticksArr = chopsticksManager.add(182500)的方式去建立182500雙筷子,使用後,再通過chopsticksManager.destroy()的方式去銷燬筷子,這個過程中我們進行了182500次筷子的建立。

為了減少一次性筷子,使用公筷,我們定義1000雙公筷。

image.png

用程式碼實現使用公筷後,使用公筷和公筷回收的情況: ``` // 筷子序列號 var serialNumber = 1; // 筷子建構函式 var Chopsticks = function (serialNumber, type) { this.serialNumber = serialNumber; // 公筷序列號 this.type = type; // 這雙公筷的使用狀態 } // 筷子管理 var chopsticksManager = (function () { var innerData = [] // 提升為內部狀態的享元池 var recycleData = [] // 公筷回收池 return { // 建立公筷 add: function (count) { for (var i = 0; i < count; i++) { innerData.push(new Chopsticks(serialNumber++)) } return innerData; }, // 使用公筷 use: function (count) { for (let i = 0; i < count; i++) { var item = innerData.pop() item.type = 'hasUsed' // 標註為已經被使用 recycleData.push(item); } }, // 回收公筷 recycle: function () { let recycleDataLength = recycleData.length for (let i = 0; i < recycleDataLength; i++) { var item = recycleData.pop() item.type = 'hasRecycled'; // 標註為已經被回收 innerData.push(item); } }, } })() // 公筷建立,公筷建立是日常客流的二倍,以防客流突然增多 var chopsticksArr = chopsticksManager.add(1000);

// 有一天客流325 chopsticksManager.use(325); // 筷子的使用 chopsticksManager.recycle(); // 筷子的回收

// 有一天客流732 chopsticksManager.use(732); // 筷子的使用 chopsticksManager.recycle(); // 筷子的回收

// 有一天客流210 chopsticksManager.use(210); // 筷子的使用 chopsticksManager.recycle(); // 筷子的回收

// 日復一日,年復一年,筷子的使用就從公筷池中使用,洗淨消毒回收 ```

在當前例子中,我們定義了筷子建構函式,然後通過chopsticksManager進行筷子的管理。通過chopsticksArr = chopsticksManager.add(1000)的方式去建立1000雙公筷,通過chopsticksManager.use(325)的方式去使用,使用後再通過chopsticksManager.recycle()的方式去洗淨消毒回收。

那麼,在整個升級改造過程中,我們節省了超十八萬雙一次性筷子。

一次性筷子是沒有享元的情況,使用公筷後,1000雙公筷相當於1000個享元,在公筷池中,我們可以進行公筷的取出,和公筷清洗和消毒後的放回。就相當於,我們在享元池中進行享元的取出和放回。

(三)行為型

9、策略模式

策略模式指的是,定義一系列的演算法,把它們一個個的封裝起來,通過傳遞一些引數,使他們可以相互替換。

舉個週末從家去咖啡館的例子:

image.png

從家去咖啡館,有跑步、騎行和漫步的方式。也就是說,從家到咖啡館,有三種策略可選擇。

(1)策略模式的極簡實現**

通過物件的鍵值對映關係,定義策略和具體實現之間的關係: var strategies = { A: xxx, B: yyy, C: zzz } 其中,ABC指的策略名稱,xxxyyyzzz指的是具體函式(演算法)。

(2)策略模式的簡單案例**

① 工具函式

當專案搭建的過程中,可以通過策略模式,封裝常用的優化函式防抖和節流。 const tools = { throttle: function (fn, time) { // ... }, debounce: function (fn,time) { // ... }, } ② 提示樣式 vue框架下頁面中的弱提示toast樣式,也可以根據型別進行樣式的預置,比如,先在style中定義預置的樣式 ```

利用`vue`的計算屬性,將傳入的型別和字串'Active'拼接組成策略,如'defaultActive'、'successActive'、'infoActive'、'warningActive'和'errorActive'

在`template`檢視端進行"策略"和樣式的關聯 ```

小結

策略模式,可以利用物件的鍵值對映關係以及函式是一等公民的特性,以key來作為策略名稱,以函式作為值定義具體演算法,利用這些javascript特性可以更為簡單的實現策略模式。

10、迭代器模式

迭代器模式,指的是提供一種方法順序訪問一個聚合物件或者陣列中的各種元素,而又不暴露該物件的內部表示。

(1)內部迭代器

內部迭代器是自動的,將回調函式傳入迭代器進行執行,訪問到每一個元素都會執行傳入迭代器中的回撥函式。

模擬內部迭代器如下: // 定義陣列原型上的mapFn內部迭代器 Array.prototype.mapFn = function (callback) { let arr = this; let newArr = [] for (let i = 0; i < arr.length; i++) { newArr[i] = callback(arr[i], i, arr) } return newArr } // 定義原始陣列 var arr = [1, 2, 3, 4, 5]; // 定義回撥函式 var callback = val => val * 2; // 執行陣列的mapFn方法呼叫回撥函式callback var newArr = arr.mapFn(callback); // 列印返回值 console.log(newArr) callback函式可以傳入三個引數,第一個引數表示當前的值,第二個引數表示當前索引,第三個引數表示正在操作的陣列。返回值為新陣列。

當前例子中,callback指的是val => val * 2,通過陣列的mapFn方法執行callback函式,返回值為新的陣列newArr

在實際的使用中,Array.prototype.mapFn的內部實現是看不到的,就像我們看不到陣列的操作mapforEach一樣,這裡如果將Array.prototype.mapFn作為黑盒子。就有如下的流程:

image.png

(2)外部迭代器

外部迭代器是非自動的,提供了next方法,需要手動的去執行next()以進行下一個元素的訪問。 ``` // 定義迭代器生成函式 function makeIterator(array) { var nextIndex = 0; return { next: function () { return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true }; } }; } // 產生迭代器 var it = makeIterator(['a', 'b']);

// 通過迭代器暴露出來的next方法,外部呼叫迭代器 console.log(it.next()) // { "value": "a", "done": false } console.log(it.next()) // { "value": "b", "done": false } console.log(it.next()) // { "value": undefined, "done": true } ``makeIterator返回next方法,每一次執行都會執行下一個迭代。done是否迭代結束,value是當前迭代獲取到的值。如果donetrue,對應的value就是undefined`。

在實際的使用中,makeIterator的內部實現是看不到的,這裡如果將makeIterator作為黑盒子。就有如下的流程:

image.png

小結

目前JavaScript已經內建了多種內部迭代器,比如forEachmapfilterreduce等,內部執行回撥函式function(value, index, currentArr){ xxxx }對每個訪問到的元素進行處理。通過generateyield配合使用也可以產生外部迭代器,通過next()方法進行下一步的執行。

11、釋出訂閱者模式

釋出訂閱者模式,我們從一條古老的街道說起

(1)有一條古老的街道

有一條古老的街道,有一天,開了一個報社,是關於財經的。(定義一個釋出者類Publish

我們叫它“財經報社”。(例項化時定義報社名稱publisherName

它有一張訂閱者登記表。(例項化時定義一個包含訂閱者名稱的登記表watcherLists

使用者登記表可以記錄訂閱者的名字。(新增訂閱者名的方法addWatcher

送報員把報紙送到每一個訂閱者手裡。(通知訂閱者的方法notify

訂閱者不訂閱時,報社可以移出訂閱者的名字。(移出訂閱者的方法removeWatcher

報社萬一哪天關門時,會清空訂閱者列表。(清空訂閱者的方法清空釋出者列表

上面的每一句話,都代表了一個虛擬碼,下面具體實現一個釋出者類:

``` class Publish { constructor(publisherName) { // 釋出者名稱 this.publisherName = publisherName; // 訂閱者列表 this.watcherLists = [] } // 新增訂閱者 addWatcher(watcher) { this.watcherLists.push(watcher) } // 通知訂閱者 notify() { const watcherLists = this.watcherLists.slice() for (let i = 0, l = watcherLists.length; i < l; i++) { watcherLists[i].update() } } // 移除訂閱者 removeWatcher(watcherName) { if (!this.watcherLists.includes(watcherName)) { return; } for (let i = 0; i < this.watcherLists.length; i++) { if (this.watcherLists[i].watcherName === watcherName) { this.watcherLists[i].removePublishers(this.publisherName) this.watcherLists.splice(i, 1) } } } // 清空訂閱者列表 clearWatchers() { const watcherLists = this.watcherLists.slice() for (let i = 0, l = watcherLists.length; i < l; i++) { watcherLists[i].removePublishers(this.publisherName) }

    this.watcherLists = []
}

} ```

財經報館開業,我們new個報館例項。 const financialNewspaper = new Publish('財經報社')

(2)來了兩個財經愛好者

有一天來了兩個財經愛好者。(定義一個訂閱者類Watcher

訂閱者是有名稱的。(例項化時定義報社名稱watcherName

訂閱者訂閱的可能不止一家報社。(例項化時定義一個包含報社(釋出者)的筆記本publishers

訂閱者收到報紙後的行為。(例項時定義定義訂閱者的行為(事件)fn

訂閱者是通過什麼樣的方式接收報紙的。(定義接收報紙(釋出者釋出的訊息)的途徑,這裡統一為信箱方式update

訂閱者可以訂閱其他報社的報紙。(添加發布者的方式addPublisher

訂閱者也可以取消某家報社的報紙。(移除釋出者的方式removePublishers

訂閱者離開這條街道時,清空報社名稱的筆記本。(清空釋出者列表clearPublishers

上面的每一句話,都代表了一個虛擬碼,下面具體實現一個訂閱者類: ``` class Watcher { constructor(watcherName, fn) { this.watcherName = watcherName; // 訂閱者名稱 this.publishers = [] // 釋出者列表 this.fn = fn // 監聽者收到訊息後的反應(事件) } // 更新自身事件(行為) update() { this.fn(); } // 添加發布者 addPublisher(publisher) { this.publishers.push(publisher) } // 移除釋出者 removePublishers(publisherName) { if (!this.publishers.includes(publisherName)) { return; } for (let i = 0; i < this.publishers.length; i++) { if (this.publishers[i].publisherName === publisherName) { this.publishers[i].removeWatcher(this.watcherName) // 通知釋出者刪除訂閱者 this.publishers.splice(index, 1) // 從釋出者列表中清除釋出者 } } } // 清空釋出者列表 clearPublishers() { const publishers = this.publishers.slice() for (let i = 0, l = publishers.length; i < l; i++) { publishers[i].removeWatcher(this.watcherName) }

    this.publishers = []
}

} 關於訂閱者,我們`new`兩個訂閱者例項。 const watcherA = new Watcher('watcherA', function () { console.log('喝著茶,看著報紙') }) // 定義訂閱者B const watcherB = new Watcher('watcherB', function () { console.log('大清早,晨讀報紙') }) ```

財經報刊添加了兩個訂閱者watcherAwatcherBfinancialNewspaper.addWatcher(watcherA) financialNewspaper.addWatcher(watcherB) // 可以打印發布者和釋出者收集的訂閱者列表 console.log(financialNewspaper, financialNewspaper.watcherLists);

兩個細心的訂閱者把財經報刊記錄在了小本本上。 watcherA.addPublisher(financialNewspaper); watcherB.addPublisher(financialNewspaper); // 可以列印訂閱者和訂閱者訂閱的報刊種類 console.log(watcherA, watcherA.publishers); console.log(watcherB, watcherB.publishers);

(3)訂閱者收到報紙

第二天,送報員就把報紙投進了門口郵箱(相當於財經報刊進行了訊息釋出) financialNewspaper.notify() // watcherA和watcherB收到報紙(訊息)後,就觸發了他們的行為 // watcherA:'喝著茶,看著報紙' // watcherB:'大清早,晨讀報紙'

(4)財經報社又來了個訂閱者

有一天財經報社來了個watcherC,也訂閱了報刊。

我們再new個訂閱者watcherC: const watcherC = new Watcher('watcherC', function () { console.log('大晚上,熬夜看報紙') }) 報社把訂閱者watcherC記錄在了登記表上。 financialNewspaper.addWatcher(watcherC)

同樣細心的訂閱者watcherC也把財經報社記錄在了小本本上。 watcherC.addPublisher(financialNewspaper);

(5)街道上又開了家體育類報社

有一天街道上又開了個體育報社。

我們先new一個體育報社。 const sportsNewspaper = new Publish('體育報社') watcherAwatcherC也是體育愛好者,所以訂閱了體育報刊。

體育報社需要登記兩個訂閱者的姓名。 sportsNewspaper.addWatcher(watcherA) sportsNewspaper.addWatcher(watcherC)

這兩訂閱者,又各自把體育報社記錄在了小本本上。 ``` watcherA.addPublisher(sportsNewspaper); watcherC.addPublisher(sportsNewspaper);

```

(6)有訂閱者取消體育報刊的報紙

訂閱者watcherC本來不喜歡運動,起初訂閱體育報刊純粹為了湊熱鬧,三天的勁頭已過,他決定取消體育報刊的報紙。 watcherC.removePublishers('sportsNewspaper')

(7)有訂閱者要離開這條街道

有一天,watcherA要出國留學,所以就從小本本上劃掉了記錄的報刊名稱,並且通知報社取消報紙的訂閱,第二天,送報員就沒再給watherA送報紙。 watcherA.clearPublishers()

這裡watcherC清掉小本本上名稱的同時,也會通知到報社,體育報社和財經報社同樣會在等級表上清除watcherC的名稱。 ``` clearPublishers() { const publishers = this.publishers.slice() for (let i = 0, l = publishers.length; i < l; i++) { publishers[i].removeWatcher(this.watcherName) }

this.publishers = []

} ```

(8)有報社要關門

歲月如梭,多年過去啦。

隨著移動網際網路的興起,紙媒受到影響,這條街道的財經報社決定關門。 financialNewspaper.clearWatchers()

第二天就不再給登記表上的訂閱者送報啦,訂閱者收到訊息後,從小本本上劃掉了財經類報刊的名字。 ``` clearWatchers() { const watcherLists = this.watcherLists.slice() for (let i = 0, l = watcherLists.length; i < l; i++) { watcherLists[i].removePublishers(this.publisherName) }

this.watcherLists = []

} ```

這裡描述了釋出者的產生、訂閱者的產生、釋出者釋出訊息的方式、訂閱者接受訊息的途徑、訂閱者接收到訊息的行為、釋出者的新增、訂閱者的新增、釋出者的離開和訂閱者的離開等關係和邏輯。程式碼具體的執行結果,還需要學友自行執行驗證。

小結

釋出訂閱者模式又叫觀察者模式,它定義了物件間的一種一對多的關係。這種關係,既指一個釋出者可以對應多個訂閱者,又可以指一個訂閱者也訂閱多個釋出者的訊息。

12、命令模式

命令者模式,指的是執行主體可以執行某些特定事件,並且,支援佇列等待、調起執行和事件撤銷等行為。

假如有個五子棋的場景,黑棋是真人,白棋是電腦,黑棋先落子,可以悔棋。

我們先來定義一個執行者主體類: ``` class CommandSubject { constructor(name) { // 命令執行者 this.executer = name; // 命令所在位置的列表 this.posList = [] // 演算步驟 this.computedStep = [] }

// 命令執行函式
execute(pos /*落子位置*/ ) {
    // 執行命令
    console.log(`棋子落在了[${pos[0]}, ${pos[1]}]的位置`)
    // 記錄位置
    this.posList.push(pos)
}
// 撤回操作
undo(step /* 撤回步數*/ ) {
    // 撤回命令
    for (let i = 0; i < step; i++) {
        // 撤回的位置
        const pos = this.posList.pop();
        console.log()
        // 撤回的操作
        console.log(`撤回[${pos[0]}, ${pos[1]}]位置的棋子`)
    }
}
// 執行演算步驟
executeComputedCommand() {
    // 請自行實現吆
}

} 我們`new`一個黑棋執行者。 var blackSubject = new CommandSubject('黑棋執行者'); 假如黑棋執行者,和電腦共對弈 `4` 步。 blackSubject.execute([3, 4]) blackSubject.execute([4, 2]) blackSubject.execute([5, 3]) blackSubject.execute([5, 4]) console.log(blackSubject.posList) ``` 此時的局勢如圖:

image.png

黑棋執行者覺得下錯了,想悔棋 2 步。 blackSubject.undo(2) 此時的局勢如圖:

image.png

下棋時可能我們還會想一下接下來會走那幾步,涉及到演算,假如我們演算了 3 步,那麼這 3 步不是一下就落子的,而是等白棋落子後,黑棋執行者才能落子,這就是命令執行的佇列問題。請自行實現吆~

如果感興趣,也可以再new一個白棋執行者,互動下棋吆~

13、模板方法模式

模板方法模式由父類和子類構成,通過父類確定整個系統的執行流程,子類負責具體的流程實現,可以通過繼承的方式重寫父類的某些方法,但是不能改變功流程的執行順序。體現了抽象與實現分離程式設計思想。

image.png

圖中,父類控制了整個系統的執行流程,子類負責具體的流程實現。

(1)經典案例飲料衝制流程

我們知道,衝制飲料一般有以下步驟:
①把水煮沸
②用沸水沖泡飲料
③把飲料倒進杯子
④加調料
示例程式碼:

    // 父類:實現泡製飲料的子類功能的流程,本次功能有4個流程,如下:
    var Beverage = function () {}
    // 然後,我們梳理衝制飲料的流程
    Beverage.prototype.boilWater = function () {
        console.log('公共流程:把水煮沸')
    }
    Beverage.prototype.brew = function () {
        throw new Error( '子類必須重寫 brew 方法' );
    }
    Beverage.prototype.pourInCup = function () {
        throw new Error( '子類必須重寫 pourInCup 方法' );
    }
    Beverage.prototype.addCondiments = function () {
        throw new Error( '子類必須重寫 addCondiments 方法' );
    } 
    // 衝制飲料
    Beverage.prototype.init = function () {
        this.boilWater();
        this.brew();
        this.pourInCup();
        this.addCondiments();
    }
    // 子類:具體實現泡製一杯茶的的流程
    var Tea = function () {}
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function () {
        console.log('用水泡茶');
    }
    Tea.prototype.pourInCup = function () {
        console.log('將茶倒進杯子');
    }
    Tea.prototype.addCondiments = function () {
        console.log('加冰糖');
    }
    var tea = new Tea();
    tea.init()

  從以上例子可以看出,父類已經制定了泡製飲料的流程,並且確定了不管哪種飲料都需要把水煮沸的公共方法boilWater,至於brewpourInCupaddCondiments泡製茶、黑咖啡、牛奶和豆漿等飲料都有所不同,由子類去具體實現。

抽象的父類已經產生,接下來就是泡製茶的子類的具體實現,子類首先繼承父類的泡製飲料的確定流程。其中,將水燒開繼承父類,brewpourInCupaddCondiments方法由子類進行重寫,至此,泡茶的流程已經完成,黑咖啡、牛奶和豆漿等飲料同理。

以上例子執行結果是:

(2)框架案例vue的主流程

vue2.0是最受歡迎的前端框架之一,以其小而美的特點,成為眾多前端小夥伴的首選。使用vue的過程中,全域性方法的定義、生命週期的使用、元件的封裝和路由的實現等都感覺隱隱約約都被一種力量牢牢鎖定,vue各個功能在使用的過程有序進行著。翻看vue原始碼時才發現: ``` import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index'

function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the new keyword') } this._init(options) } // Vue類由各種initMixin、stateMixin、eventsMixin、lifecycleMixin和renderMixin的方法有序的混入各種功能 initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)

export default Vue `` 我們發現,Vue本質上是一個建構函式,在其new的時候,會執行內部唯一的初始化方法this._init`。

初始化方法在initMixin中實現: ```

Vue.prototype._init = function (options?: Object) { const vm: Component = this // ... initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } } } `` 可以看出,初始化this._init方法是由如圖的一些方法確定有序執行的。vue的建立過程中的初始化方法this_init`就是一種模板方法模式。

小結

模板方法模式是眾多設計模式之一,解決的主要業務場景是父類建立確定的子類功能或者任務的執行流程,子類繼承的時候可以重寫父類的某些方法。

14、職責鏈模式

職責鏈模式,指的是由擁有處理能力的職責節點物件組成一個鏈條,一個請求從鏈條的開始或者中間進入,都有機會被後續的職責節點物件處理。進入職責鏈的請求,會沿著後續鏈條被傳遞,直到請求被處理才會終止傳遞。

身在帝都,每天擠地鐵去搬磚,發現月初、月中和月末地鐵的票價都不一樣。

檢視規則才發現,乘車消費超過150元打五折,超過100元打八折,不足100元不打折。

這裡通過職責鏈模擬一個處理訂單金額的功能,該方法傳入消費總金額和當前車程的常規票價。

先看流程圖:

image.png

先進入總消費大於150元的流程,當前消費金額是否大於150元,返回金額,請求結束。如果不是,進入下一個總消費大於100元的流程。

進入總消費大於100元的流程,當前消費金額是否大於100元,返回金額,請求結束。如果不是,進入下一個總消費不足100元的流程。

在總消費不足100元的流程中,所有的請求都會被處理。

下面進行具體的程式碼實現: `` // 1、定義各職責節點物件 // 定義超過150元的職責節點物件 var consumption150 = function (consumption, fare) { if (consumption > 150) { console.log(地鐵總消費大於150元,本次${fare}元的票價折扣價${fare * 0.5}); } else { return 'nextProcess'; // 傳遞給下一個流程 } }; // 定義超過100元的職責節點物件 var consumption100 = function (consumption, fare) { if (consumption > 100) { console.log(地鐵總消費大於100元,本次${fare}元的票價折扣價${fare * 0.8}); } else { return 'nextProcess'; // 傳遞給下一個流程 } }; // 定義不足100元的職責節點物件 var consumptionNormal = function (consumption, fare) { console.log(地鐵總消費不足100元,本次${fare}元的票價依然收費${fare}元`); };

// 2、設定鏈條建構函式 var Chain = function (selfFn) { this.selfFn = selfFn; // 自身函式 this.process = null; // 下一個職責節點 }; Chain.prototype.setNextProcess = function (process) { return this.process = process; // 設定當前節點物件的下一個節點物件 }; Chain.prototype.handleRequest = function () { // 執行自身函式,並返回執行結果 var ret = this.selfFn.apply(this, arguments); // 返回nextProcess表示當前職責節點不能處理請求,此時將請求交個下一個職責節點 if (ret === 'nextProcess') { return this.process && this.process.handleRequest.apply(this.process, arguments); } return ret; }; // 建立節點,包含自身selfFn和是否成功請求的標誌nextProcess var chainconsumption150 = new Chain(consumption150); var chainconsumption100 = new Chain(consumption100); var chainconsumptionNormal = new Chain(consumptionNormal);

// 3、設定節點間的鏈條關係,形成職責鏈 chainconsumption150.setNextProcess(chainconsumption100); chainconsumption100.setNextProcess(chainconsumptionNormal); ```

(1)職責鏈可以從任意節點進入

現在可以通過chainconsumption150.handleRequest(120, 8)的方式進入到職責鏈的第一個職責節點物件中。

也可以通過chainconsumption100.handleRequest(120, 8)的方式進入到職責鏈的第二個職責節點物件中。

(2)職責鏈可以進行擴充套件

假如,總消費超過300元以後,可以打四折。

①定義職能節點物件 var consumption300 = function (consumption, fare) { if (consumption > 300) { console.log(`地鐵總消費大於300元,本次${fare}元的票價折扣價${fare * 0.4}`); } else { return 'nextProcess'; // 傳遞給下一個流程 } }; ②建立鏈條節點

var chainconsumption300 = new Chain(consumption300); ③把節點塞入到鏈條中 chainconsumption300.setNextProcess(chainconsumption150);

小結

職責鏈模式,支援鏈條長度的擴充套件,也支援在鏈條中進入的位置。

15、中介者模式

中介者模式的作用就是解決物件與物件之間錯綜複雜的互動關係,增加一箇中介者以後,所有相關的物件都通過中介者物件來通訊,當一個物件發生改變後,只需要通知中介者物件即可。

(1)一個賣主,多個買主

假設有賣家A有一套面積:100平米,價格:20000元/平米的房子急需出售,他目前知道有三個買主在找房,對應關係如圖:

image.png

通過程式碼實現賣家找買家: ``` // 賣家類 var Seller = function (name, info) { this.name = name; this.info = info; } // 賣家找買家的函式 Seller.prototype.match = function (buyer) { // 定義買家要求 const buyerDemand = buyer.demand; // 獲取需求數字 const reg = /\d+/ // 1、買家的要求 let buyerRequestArea = buyerDemand.area.match(reg); buyerRequestArea = parseInt(buyerRequestArea); let buyerRequestprice = buyerDemand.price.match(reg); buyerRequestprice = parseInt(buyerRequestprice); // 2、賣家的條件 let sellerOwnArea = this.info.area.match(reg); sellerOwnArea = parseInt(sellerOwnArea); let sellerOwnprice = this.info.price.match(reg); sellerOwnprice = parseInt(sellerOwnprice); return sellerOwnArea >= buyerRequestArea && sellerOwnprice <= buyerRequestprice; } // 買家類 var Buyer = function (name, demand) { this.name = name; this.demand = demand; }

// 定義賣家 var sellA = new Seller('賣家A', { area: '100平米', // 賣家尺寸 price: '20000元/平米' // 賣家要價 });

var buyerX = new Buyer('買家X', { area: '110平米', // 買家要求尺寸 price: '10000元/平米' // 買家最高願意支付 }) var buyerY = new Buyer('買家Y', { area: '120平米', // 買家要求尺寸 price: '30000元/平米' // 買家最高願意支付 }) var buyerZ = new Buyer('買家Z', { area: '99平米', // 買家要求尺寸 price: '30000元/平米' // 買家最高願意支付 }) // 賣家開始找買主 console.log(sellA.match(buyerX)); // true:沒找到 console.log(sellA.match(buyerY)); // true:沒找到 console.log(sellA.match(buyerZ)); // true:找到了 ```

當前例子中,先定義賣家類,併為賣家定義match方法去匹配買家,如果其售賣面積大於買家要求,且售賣價格低於買家最高願意支付,我們認為該買主是意向客戶。

當前例子中剛好找到第三個買家Z的時候,就找到了。但是實際情況可能是找了幾十個也沒找到合適的意向客戶。

(2)多個賣主,多個買主

假設又有賣主B賣主C也加入了賣方行列中,此時的對應關係如圖:

image.png

如果,我們依然按照以上的方式為賣主B賣主C尋找買主,那麼,此時的對應關係就已經開始複雜起來。如果有成千上萬個賣主買主在進行交易的匹配,交易狀況就更加複雜,一個賣主可能會和幾十個買主溝通篩選,一個買主也可能和幾十個賣主溝通篩選。

這個時候,就有必要通過中介者模式進行改造了,為了展示主要邏輯,以下去掉價格和平米單價的單位。 ``` // 賣家類 var Seller = function (name, info) { this.name = name; this.info = info; }

// 買家類 var Buyer = function (name, demand) { this.name = name; this.demand = demand; }

// 引入中介者 var broker = (function () { var sellerList = []; var buyerList = []; var operations = {} operations.addSellers = function (seller) { sellerList.push(seller) }

operations.addBuyers = function (buyer) {
    buyerList.push(buyer)
}

operations.findBuyer = function (seller) {
    const result = []
    // 遍歷所有的買家
    buyerList.map(v => {
        console.log(v.demand, seller);
        if (seller.info.price <= v.demand.price && seller.info.area >= v.demand.area) {
            result.push(v);
        }
    })
    return result
}

operations.findSeller = function (buyer) {
    const result = []
    // 遍歷所有的買家
    sellerList.map(v => {
        if (v.info.price <= buyer.demand.price && v.info.area >= buyer.demand.area) {
            result.push(v);
        }
    })
    return result;
}

return operations;

})()

// 定義賣家,並將其新增到中介者賣家列表中 var sellA = new Seller('賣家A', { area: 100, // 賣家尺寸 price: 20000 // 賣家要價 }); var sellB = new Seller('賣家B', { area: 90, // 賣家尺寸 price: 18000 // 賣家要價 });

var sellC = new Seller('賣家C', { area: 120, // 賣家尺寸 price: 22000 // 賣家要價 });

broker.addSellers(sellA) broker.addSellers(sellB) broker.addSellers(sellC)

// 定義買家,並將其新增到中介者買家列表中 var buyerX = new Buyer('買家X', { area: 110, // 買家要求尺寸 price: 10000 // 買家最高願意支付 }) var buyerY = new Buyer('買家Y', { area: 80, // 買家要求尺寸 price: 30000 // 買家最高願意支付 }) var buyerZ = new Buyer('買家Z', { area: 100, // 買家要求尺寸 price: 30000 // 買家最高願意支付 })

broker.addBuyers(buyerX) broker.addBuyers(buyerY) broker.addBuyers(buyerZ) ``` 例子中,我們除了定義賣家類和買家類,我們還引入了中介者,中介者擁有賣家資訊列表,也擁有買家資訊列表。當有賣家需要賣方時,可以將房屋資訊和個人姓名留給中介者,中介者將其推入到賣家資訊列表中。當有買家需要買房時,可以將購買需求留給中介者,中介者將其推入到買家需求列表中。

有一天,賣家A告訴中介者,他著急用錢,他的房子著急出手。於是中介者開始幫其尋找買主: var buyers = broker.findBuyer(sellA)

有一天,買家Z告訴中介者,他現在手頭有錢了,想全款買套房。於是中介者開始幫其尋找買主: var sellers = broker.findSeller(buyerZ)

小結

我們發現,引入中介者以後,賣家和買家再也不用去為尋找買家或者賣家而煩惱,中介者擁有大量的賣主和買主資訊,為其快速精準匹配。這大概也是中介這個職業興起,並且長盛不衰的原因之一。

16、狀態模式

狀態模式,指的是事物內部狀態的變化,會導致事物具體行為的變化。並且,狀態的切換可以是迴圈的。最簡單的例子是生活中的開關,基本都是狀態模式的使用。

image.png

(1)利用例項物件的切換

// 定義燈類 var Light = function () { this.currState = stateManager.off; // 燈的狀態 this.button = null; // 開關 }; Light.prototype.init = function () { var button = document.createElement('button'), self = this; button.innerHTML = '關燈'; this.button = document.body.appendChild(button); this.button.onclick = function () { self.currState.changeState.call(self); // 把請求委託給 stateManager 狀態機 } }; // 定義燈的狀態管理物件 var stateManager = { off: { changeState: function () { this.button.innerHTML = '開燈'; this.currState = stateManager.on; } }, on: { changeState: function () { this.button.innerHTML = '關燈'; this.currState = stateManager.off; } } }; // 例項化燈 var light = new Light(); // 登初始化 light.init(); 在當前例子中,首先定義燈類Light,其中有屬性currState表示當前狀態,button表示開關。定義的init方法會首先建立開關,再為開關繫結切換開關狀態的函式changeState

定義的狀態管理物件中包含屬性offon的具體行為物件,每個行為的執行都是通過其中的changeState函式來實現,該函式觸發時就會將當前燈的狀態進行切換。

(2)利用陣列的有序性

``` // 定義燈的類 var Light = function () { this.currentIndex = 0; // 設定初始索引 this.button = null; // 開關 }; Light.prototype.init = function () { var button = document.createElement('button'), self = this; button.innerHTML = '關燈'; this.button = document.body.appendChild(button); this.button.onclick = function () { excuteStateFn(self); } }; // 定義狀態狀態切換列表 var stateList = [ function changeState(light) { light.button.innerHTML = '開燈'; }, function changeState(light) { light.button.innerHTML = '關燈'; } ] // 定義狀態切換執行函式 function excuteStateFn(light) { light.currentIndex >= stateList.length && (light.currentIndex = 0); // 進行邊界狀態的控制 stateListlight.currentIndex++ // 切換狀態

} // 例項化燈 var light = new Light(); // 燈進行初始化 light.init(); ```

在當前例子中,首先定義燈類Light,其中有屬性currentIndex表示行為對應的索引,button表示開關。定義的init方法會首先建立開關,再為開關繫結切換開關狀態的函式excuteStateFn(self)

定義的狀態切換列表中stateList包含陣列元素offon的具體行為函式。

再定義行為切換執行函式excuteStateFn,每個行為的執行都是通過執行當前索引對應的行為函式stateList[light.currentIndex++](light)來實現的,通過修改當前索引的值來切換下一次執行的狀態索引。

小結

狀態模式的實現不止一種實現思路,可以利用行為函式執行時修改當前例項物件的狀態實現,也可以利用陣列天然的有序性通過索引的改變指向對應的執行函式,當然,還可能有其他實現方式。只要遵循狀態模式狀態的切換可以是迴圈的,任何實現都是正確的。

總結

以上簡單介紹了十六種設計模式,當然除此之外,還有其他的設計模式。也許,不遠的將來,會有新的,被眾多人所承認的設計模式產生。

通過以上介紹我們可以先略微掌握設計模式的,不斷修道,終會將設計模式內化,在合適的場景,可能會起到事半功倍的效果,扳子、鉗子、改錐、電鑽我們都有,萬一哪天要打個孔,上個螺絲,就變得非常簡單。

學習設計模式也是,在某些錯綜複雜的場景中,拿出一兩個設計模式思想,高效處理業務難點,就如打個孔,上個螺絲那麼簡單。

參考資料

《JavaScript》設計模式與開發實踐

寫在最後

2023年,新的一年,新的征程,希望各位學友狡兔三窟,即使被裁也能很快找到下一個,一起加油。

2023年,希望自己能夠有高質量的文章輸出,持續分享。

文中紕漏在所難免,如有紕漏請貴手留言

本文如有幫助,點贊就是對我最大的肯定和支援吆❤