精準而優雅的設計模式

構造器

名字嚇人。結果每天使用。javascript

  • 場景: 公司員工信息錄入系統、少許員工錄入
const lilei = {
  name: '李磊',
  age: 25,
  career: 'coder'
}
// 構造器方式
function User(name, age, career) {
  this.name = name;
  this.age = age;
  this.career = career;
}
const lilei = new User('李磊', 25, 'coder');
複製代碼
  • 程序自動地去讀取數據庫裏面一行行的員工信息,而後把拿到的姓名、年齡職業等字段塞進User函數裏,進行一個簡單的調用。css

  • 構造器是否是將 name、age、career 賦值給對象的過程封裝,確保了每一個對象都具有這些屬性,確保了共性的不變,同時將 name、age、career 各自的取值操做開放html

簡單工廠模式

  • 場景: 區分員工的職業。若是是碼農就寫Bug。若是老闆就會所。
// 構造器方式
function User(name, age, career, work) {
  this.name = name;
  this.age = age;
  this.career = career;
  this.work = work;
}
function Factory(name, age, career){
  let work;
  swtich(career){
    case 'coder':
    	work = '寫Bug';
    	break;
    case 'boss':
    	work = '會所';
    	break;
    default:
    	break;
  }
 	return new User(name, age, career, work)
}

const pro = new Factory('pro', 18, 'boss');
複製代碼

總結: 工廠模式的簡單之處,在於它的概念相對好理解:將建立對象的過程單獨封裝,這樣的操做就是工廠模式。同時它的應用場景也很是容易識別:有構造函數的地方,咱們就應該想到簡單工廠;在寫了大量構造函數、調用了大量的 new、自覺很是不爽的狀況下,咱們就應該思考是否是能夠掏出工廠模式重構咱們的代碼了java

單例模式

  • 只有一個實例
class Modal{
		static getModal(){
			if(!Modal.modal){
					Modal.modal  = 123;
			}
			return Modal.modal;
		}
}
const modal1 = Modal.getModal();
const modal2 = Modal.getModal();
modal1 === modal2; // true
複製代碼
  • 場景: UI框架其中的modal只有一個實例
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>單例模式彈框</title>
</head>
<style> #modal { width: 200px; height: 200px; line-height: 200px; text-align: center; border-radius: 10px; background-color: #f2f2f2; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } </style>
<body>
<div class="btnBox">
    <button id='open'>打開彈框</button>
    <button id='close'>關閉彈框</button>
</div>

</body>
<script> // 閉包方式 // const Modal = (function () { // let modal = null; // return function() { // if (!modal){ // modal = document.createElement('div'); // modal.id = 'modal'; // modal.style.display = 'none'; // modal.innerHTML = "惟一彈窗"; // document.body.appendChild(modal); // // } // return modal; // } // })(); // class方式 class Modal { static getModal() { if (!Modal.modal) { Modal.modal = document.createElement('div'); Modal.modal.id = 'modal'; Modal.modal.style.display = 'none'; Modal.modal.innerHTML = '惟一彈窗'; document.body.appendChild(Modal.modal); } return Modal.modal; } } // 點再多下也只是有Modal document.getElementById('open').addEventListener('click', function () { const modal = Modal.getModal(); modal.style.display = 'block'; }); // 點擊關閉按鈕隱藏模態框 document.getElementById('close').addEventListener('click', function () { const modal = Modal.getModal(); modal.style.display = 'none'; }); </script>
</html>
複製代碼

原型模式

  • 原型是 把全部的對象共用的屬性所有放在堆內存的一個對象中(共用屬性組成的對象),而後讓每個對象的__proto__存儲這個(共用屬性組成的對象)的地址。而這個共用屬性就是原型。原型出現的目的就是爲了減小沒必要要的內存消耗。ios

  • 原型鏈就是對象經過__proto__向當前實例所屬類的原型上查找屬性或方法的機制,若是找到Object的原型上仍是沒有找到想要的屬性或者是方法則查找結束,最終會返回undefined,終點是null。面試

  • 原型模式不只是一種設計模式,它仍是一種編程範式,是 JavaScript 面向對象系統實現的根基。算法

function Dog() {
}
// 原型增長屬性和方法
Dog.prototype.name = 'pro';
Dog.prototype.eat = () => {
  console.log(123);
}
複製代碼

裝飾器模式

只添加,不修改就是裝飾器模式了數據庫

  • 場景: 初始需求是每一個業務中的按鈕在點擊後都彈出「您還未登陸哦」的彈框。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>按鈕點擊需求1.0</title>
</head>
<style> #modal { height: 200px; width: 200px; line-height: 200px; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); border: 1px solid black; text-align: center; } </style>
<body>
	<button id='open'>點擊打開</button>
	<button id='close'>關閉彈框</button>
</body>
<script> // 彈框建立邏輯,這裏咱們複用了單例模式面試題的例子 const Modal = (function() { let modal = null return function() { if(!modal) { modal = document.createElement('div') modal.innerHTML = '您還未登陸哦~' modal.id = 'modal' modal.style.display = 'none' document.body.appendChild(modal) } return modal } })() // 點擊打開按鈕展現模態框 document.getElementById('open').addEventListener('click', function() { // 未點擊則不建立modal實例,避免沒必要要的內存佔用 const modal = new Modal() modal.style.display = 'block' }) // 點擊關閉按鈕隱藏模態框 document.getElementById('close').addEventListener('click', function() { const modal = document.getElementById('modal') if(modal) { modal.style.display = 'none' } }) </script>
</html>
複製代碼
  • 忽然修改需求:彈框被打開後把按鈕的文案改成「快去登陸」,同時把按鈕置灰。存在幾百按鈕且同時他們不是組件且有複雜業務狀況下。不去關心它現有的業務邏輯是啥樣的。對它已有的功能作個拓展,只關心拓展出來的那部分新功能如何實現

爲了避免被已有的業務邏輯干擾,當務之急就是將舊邏輯與新邏輯分離,把舊邏輯抽出去編程

// 將展現Modal的邏輯單獨封裝
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}
// 新增邏輯
// 按鈕文案修改邏輯
function changeButtonText() {
    const btn = document.getElementById('open')
    btn.innerText = '快去登陸'
}

// 按鈕置灰邏輯
function disableButton() {
    const btn =  document.getElementById('open')
    btn.setAttribute("disabled", true)
}

// 新版本功能邏輯整合
function changeButtonStatus() {
    changeButtonText()
    disableButton()
}

document.getElementById('open').addEventListener('click', function() {
    openModal()
    changeButtonStatus()
})
複製代碼
  • 使用ES6面向對象寫法
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>按鈕點擊需求1.0</title>
</head>
<style> #modal { height: 200px; width: 200px; line-height: 200px; position: fixed; left: 50%; top: 50%; border-radius: 10px; transform: translate(-50%, -50%); border: 1px solid black; text-align: center; } </style>
<body>
<button id='open'>點擊打開</button>
<button id='close'>關閉彈框</button>
</body>
<script> // 彈框建立邏輯,這裏咱們複用了單例模式面試題的例子 const Modal = (function() { let modal = null; return function() { if(!modal) { modal = document.createElement('div'); modal.innerHTML = '您還未登陸哦~'; modal.id = 'modal'; modal.style.display = 'none'; document.body.appendChild(modal) } return modal } })(); // 定義打開按鈕 class OpenButton { onClick() { const modal = new Modal(); modal.style.display = 'block'; } } // 定義按鈕對應的裝飾器 class Decorator{ // 將按鈕傳入 constructor(open_button) { this.open_button = open_button; } onClick(){ this.open_button.onClick(); this.changeButtonStatus(); } changeButtonStatus() { this.disableButton(); this.changeButtonText(); } disableButton() { const btn = document.getElementById('open'); btn.setAttribute('disabled', true); } changeButtonText() { const btn = document.getElementById('open'); btn.innerText = '快去登陸' } } // 點擊打開按鈕展現模態框 document.getElementById('open').addEventListener('click', function() { // 未點擊則不建立modal實例,避免沒必要要的內存佔用 const openButton = new OpenButton(); const decorator = new Decorator(openButton); decorator.onClick(); }); // 點擊關閉按鈕隱藏模態框 document.getElementById('close').addEventListener('click', function() { const modal = document.getElementById('modal'); if(modal) { modal.style.display = 'none' } }) </script>
</html>
複製代碼

適配模式

適配器模式經過把一個類的接口變換成客戶端所期待的另外一種接口,能夠幫咱們解決不兼容的問題。axios

  • 場景: iPhoneX沒有圓頭耳機孔、轉接頭是個適配模式。

把一個(iPhone X)的接口(方形)變換成客戶端(用戶)所期待的另外一種接口(圓形)

  • axios能在網頁和Nodejs不一樣環境使用就是使用了適配模式

代理模式

代理模式,式如其名——在某些狀況下,出於種種考慮/限制,一個對象不能直接訪問另外一個對象,須要一個第三者(代理)牽線搭橋從而間接達到訪問目的,這樣的模式就是代理模式

代理服務器 = 代理模式

  • 事件代理也是代理模式的一種

事件代理,多是代理模式最多見的一種應用方式,也是一道實打實的高頻面試題。它的場景是一個父元素下有多個子元素。

需求: 點擊每一個 a 標籤,均可以彈出「我是xxx」這樣的提示。好比點擊第一個 a 標籤,彈出「我是連接1號」這樣的提示

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>事件代理</title>
</head>
<body>
<div id="father">
    <a href="#">連接1號</a>
    <a href="#">連接2號</a>
    <a href="#">連接3號</a>
    <a href="#">連接4號</a>
    <a href="#">連接5號</a>
    <a href="#">連接6號</a>
</div>
<script> const father = document.getElementById('father'); father.addEventListener('click',(e) => { if (e.target.tagName === 'A'){ e.preventDefault(); alert(`我是${e.target.innerText}`); } }) </script>
</body>
</html>
複製代碼

策略模式

需求:

  • 當價格類型爲「預售價」時,滿 100 - 20,不滿 100 打 9 折
  • 當價格類型爲「大促價」時,滿 100 - 30,不滿 100 打 8 折
  • 當價格類型爲「返場價」時,滿 200 - 50,不疊加
  • 當價格類型爲「嚐鮮價」時,直接打 5 折

轉成字段

預售價 - pre
大促價 - onSale
返場價 - back
嚐鮮價 - fresh
複製代碼

當初的我對prd處理爲:

// 詢價方法,接受價格標籤和原價爲入參
function askPrice(tag, originPrice) {

  // 處理預熱價
  if(tag === 'pre') {
    if(originPrice >= 100) {
      return originPrice - 20
    } 
    return originPrice * 0.9
  }
  
  // 處理大促價
  if(tag === 'onSale') {
    if(originPrice >= 100) {
      return originPrice - 30
    } 
    return originPrice * 0.8
  }
  
  // 處理返場價
  if(tag === 'back') {
    if(originPrice >= 200) {
      return originPrice - 50
    }
    return originPrice
  }
  
  // 處理嚐鮮價
  if(tag === 'fresh') {
     return originPrice * 0.5
  }
}
複製代碼

如今的我會採用策略模式:

// 定義一個詢價處理器對象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};

// 詢價函數
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}
// 如要增長需求
priceProcessor.newUser = function (originPrice) {
  if (originPrice >= 100) {
    return originPrice - 50;
  }
  return originPrice;
}
複製代碼

一個函數只作一件事、遇到 Bug 時,就能夠作到「頭痛醫頭,腳痛醫腳」,而沒必要在龐大的邏輯海洋裏費力去定位究竟是哪塊不對。

策略模式就是定義一系列的算法,把它們一個個封裝起來, 而且使它們可相互替換

策略模式·延展: 替代多個ifelse的方案

1. 早早的 return 代替 if else

看看下面的代碼。嵌套式的 if 判斷的代碼是很醜陋的,很難控制,很難定位 bug。若是你嵌套得太多層,層次太深,並且若是你的電腦屏幕過小,都很難展現完整的語句。你必須用鼠標滾動屏幕才能顯示出來。

const isBabyPet = (pet, age) => {
  if (pet) {
    if (isPet(pet)) {
      console.log(‘It is a pet!’);
      
      if (age < 1) {
        console.log(‘It is a baby pet!’);
      }
    } else {
      throw new Error(‘Not a pet!’);
    }
  } else {
    throw new Error(‘Error!’);
  }
};
複製代碼

若是解決上面這個問題呢?就是要早早地 return。若是遇到錯誤,或者無效的狀況,咱們早早地 return 或者拋出錯誤,就會少一些判斷,且看下面的代碼:

const isBabyPet = (pet, age) => {
  if (!pet) throw new Error(‘Error!’);
  if (!isPet(pet)) throw new Error(‘Not a pet!’);
  
  console.log(‘It is a pet!’);
  if (age < 1) {
    console.log(‘It is a baby pet!’);
  }
};

複製代碼

2. 使用 Array.includes 假設您須要檢查動物是不是寵物,以下所示:

const isPet = animal => {
  if (animal === ‘cat’ || animal === ‘dog’) {
    return true;
  }
  
  return false;
};
複製代碼

上面的代碼中:動物若是是貓或者狗就是寵物,若是還要加上其餘的呢,好比蛇,鳥。這個時候你可能會再加上相似這樣的判斷 || animal=== 'snake'。

其實咱們能夠用 Array.includes 代替它,好比:

const isPet = animal => {
  const pets = [‘cat’, ‘dog’, ‘snake’, ‘bird’];
  
  return pets.includes(animal);
};
複製代碼

3. 在函數中使用參數默認值 咱們在定義一個函數的時候,你一般會肯定你的參數是非空的(null 或 undefined),若是是爲空,咱們會爲它設置一個默認值,咱們可能會這樣作:

const pets = [
  { name: ‘cat’,   nLegs: 4 },
  { name: ‘snake’, nLegs: 0 },
  { name: ‘dog’,   nLegs: 4 },
  { name: ‘bird’,  nLegs: 2 }
];
const check = (pets) => {
  for (let i = 0; i < pets.length; i++) {
    if (pets[i].nLegs != 4) {
      return false;
    }
  }
  return true;
}
check(pets); // false
複製代碼

咱們會用到 for 去循環遍歷這個數組,而後再用 if 來判斷。

其實一條語句就能夠簡化:

let areAllFourLegs = pets.every(p => p.nLegs === 4);
複製代碼

6. 用索引代替 switch…case 下面的 switch 語句將返回給定普通寵物的品種。

const getBreeds = pet => {
  switch (pet) {
    case ‘dog’:
      return [‘Husky’, ‘Poodle’, ‘Shiba’];
    case ‘cat’:
      return [‘Korat’, ‘Donskoy’];
    case ‘bird’:
      return [‘Parakeets’, ‘Canaries’];
    default:
      return [];
  }
};
let dogBreeds = getBreeds(‘dog’); //[「Husky」, 「Poodle」, 「Shiba」]
複製代碼

這裏寫了好多 case return,看到這樣的代碼,咱們就要想,能不能優化它。 看看下面更清潔的方法。

const breeds = {
  ‘dog’: [‘Husky’, ‘Poodle’, ‘Shiba’],
  ‘cat’: [‘Korat’, ‘Donskoy’],
  ‘bird’: [‘Parakeets’, ‘Canaries’]
};
const getBreeds = pet => {
  return breeds[pet] || [];
};
let dogBreeds = getBreeds(‘cat’); //[「Korat」, 「Donskoy」]
複製代碼

咱們先分類組合造成一個對象 breeds ,這樣對象的索引就是動物的名稱,而值就是動物的品種,咱們在使用getBreeds的時候直接傳入索引就好,就會返回出它的種類。 擴展結束·相信你會避免大量的if

狀態模式

原理跟策略模式類似。故不展開

觀察者模式

觀察者模式,是全部 JavaScript 設計模式中使用頻率最高,面試頻率也最高的設計模式,因此說它十分重要——若是我是面試官,考慮到面試時間有限、設計模式這塊不能多問,我可能在考查你設計模式的時候只會問觀察者模式這一個模式。該模式的權重極高,咱們此處會花費兩個較長的章節把它掰碎嚼爛了來掌握。

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知全部觀察者對象,使它們可以自動更新。

// 定義發佈者類
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增長訂閱者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除訂閱者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知全部訂閱者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
}
// 定義訂閱者類
class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}
複製代碼
  • 面試題: Vue雙向綁定
// observe方法遍歷幷包裝對象屬性
function observe(target) {
    // 若target是一個對象,則遍歷它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法會給目標屬性裝上「監聽器」
            defineReactive(target, key, target[key])
        })
    }
}

// 定義defineReactive方法
function defineReactive(target, key, val) {
    // 屬性值也多是object類型,這種狀況下須要調用observe進行遞歸遍歷
 		const dep = new Dep()
    observe(val)
    // 爲當前屬性安裝監聽器
    Object.defineProperty(target, key, {
         // 可枚舉
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 監聽器函數
        set: function (value) {
            // 通知全部訂閱者
            dep.notify()
        }
    });
}

// 定義訂閱者類Dep
class Dep {
    constructor() {
        // 初始化訂閱隊列
        this.subs = []
    }
    
    // 增長訂閱者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知訂閱者(是否是全部的代碼都似曾相識?)
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}
複製代碼

觀察者模式與發佈-訂閱模式的區別是什麼?

全部的開發者拉了一個羣,直接把需求文檔丟給每一位羣成員,這種發佈者直接觸及到訂閱者的操做,叫觀察者模式。但若是把需求文檔上傳到了公司統一的需求平臺上,需求平臺感知到文件的變化、自動通知了每一位訂閱了該文件的開發者,這種發佈者不直接觸及到訂閱者、而是由統一的第三方來完成實際的通訊的操做,叫作發佈-訂閱模式

迭代器模式

迭代器模式是設計模式中少有的目的性極強的模式: 遍歷

  • 任何數據結構只要具有Symbol.iterator屬性,就能夠被遍歷
// 編寫一個迭代器生成函數
function *iteratorGenerator() {
    yield '1號選手'
    yield '2號選手'
    yield '3號選手'
}

const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()
複製代碼

Thanks for reading

  • 如有錯誤,歡迎在評論區指正
  • 更好解決方案,至關歡迎指導
  • 幫到了您,點個贊再走吧~😊
相關文章
相關標籤/搜索