前端開發中常用的幾種設計模式(前端開發中用到哪些設計模式)
設計模式概覽
設計模式是對軟件設計開發過程中反復出現的某類問題的通用解決方案。設計模式更多的是指導思想和方法論,而不是現成的代碼,當然每種設計模式都有每種語言中的具體實現方式。學習設計模式更多的是理解各種模式的內在思想和解決的問題,畢竟這是前人無數經驗總結成的最佳實踐,而代碼實現則是對加深理解的輔助。
設計模式可以分為三大類:
結構型模式(Structural Patterns):?通過識別系統中組件間的簡單關系來簡化系統的設計。
創建型模式(Creational Patterns):?處理對象的創建,根據實際情況使用合適的方式創建對象。常規的對象創建方式可能會導致設計上的問題,或增加設計的復雜度。創建型模式通過以某種方式控制對象的創建來解決問題。
行為型模式(Behavioral Patterns):用于識別對象之間常見的交互模式并加以實現,如此,增加了這些交互的靈活性。
上述中一共有23種設計模式,但我們作為前端開發人員,需要了解的大概有以下10種。
前端需要了解的設計模式(10種)
創建型模式
故名思意,這些模式都是用來創建實例對象的。
1. 工廠模式
我們從簡單的開始。 簡單工廠模式是由一個工廠對象決定創建出哪一種產品類的實例。
上圖為例,我們構造一個簡單的汽車工廠來生產汽車:
// 汽車構造函數 function SuzukiCar(color) { this.color = color; this.brand = 'Suzuki'; } // 汽車構造函數 function HondaCar(color) { this.color = color; this.brand = 'Honda'; } // 汽車構造函數 function BMWCar(color) { this.color = color; this.brand = 'BMW'; } // 汽車品牌枚舉 const BRANDS = { suzuki: 1, honda: 2, bmw: 3 } /** * 汽車工廠 */ function CarFactory() { this.create = (brand, color)=> { switch (brand) { case BRANDS.suzuki: return new SuzukiCar(color); case BRANDS.honda: return new HondaCar(color); case BRANDS.bmw: return new BMWCar(color); default: break; } } }
使用一下我們的工廠:
const carFactory = new CarFactory(); const cars = []; cars.push(carFactory.create(BRANDS.suzuki, 'brown')); cars.push(carFactory.create(BRANDS.honda, 'grey')); cars.push(carFactory.create(BRANDS.bmw, 'red')); function sayHello() { console.log(`Hello, I am a ${this.color} ${this.brand} car`); } for (const car of cars) { sayHello.call(car); }
輸出結果:
Hello, I am a brown Suzuki car Hello, I am a grey Honda car Hello, I am a red BMW car
使用工廠模式之后,不再需要重復引入一個個構造函數,只需要引入工廠對象就可以方便的創建各類對象。
2. 單例模式
首先我們需要理解什么是單例?
單:指的是一個。
例:指的是創建的實例。
單例:指的是創建的總是同一個實例。也就是使用類創建的實例始終是相同的。
先看下面的一段代碼:
class Person{ constructor(){} } let p1 = new Person(); let p2 = new Person(); console.log(p1===p2) //false
上面這段代碼,定義了一個Person類,通過這個類創建了兩個實例,我們可以看到最終這兩個實例是不相等的。也就是說,通過同一個類得到的實例不是同一個(這本就是理所應當),但是如果我們想始終得到的是同一個實例,那么這就是單例模式。那么下面就該介紹如何實現單例模式了:
想要實現單例模式,我們需要注意兩點:
需要使用return。使用new的時候如果沒有手動設置return,那么會默認返回this。但是,我們這里要使得每次返回的實例相同,也就是需要手動控制創建的對象,因此這里需要使用return。
我們需要每次return的是同一個對象。也就是說實際上在第一次實例的時候,需要把這個實例保存起來。再下一個實例的時候,直接return這個保存的實例。因此,這里需要用到閉包了。
const Person = (function(){ let instance = null; return class{ constructor(){ if(!instance){ //第一次創建實例,那么需要把實例保存 instance = this; }else{ return instance; } } } })() let p3 = new Person(); let p4 = new Person(); console.log(p3===p4) //true
從上面的代碼中,我們可以看到在閉包中,使用instance變量來保存創建的實例,每次返回的都是第一次創建的實例。這樣的話就實現了無論創建多少次,創建的都是同一個實例,這就是單例模式。
3. 原型模式
通俗點講就是創建一個共享的原型,并通過拷貝這些原型創建新的對象。
在我看來,其實原型模式就是指定新創建對象的模型,更通俗一點來說就是我想要新創建的對象的原型是我指定的對象。
最簡單的原型模式的實現就是通過Object.create()。Object.create(),會使用現有的對象來提供新創建的對象的__proto__。例如下方代碼:
let person = { name:'hello', age:24 } let anotherPerson = Object.create(person); console.log(anotherPerson.__proto__) //{name: "hello", age: 24} anotherPerson.name = 'world'; //可以修改屬性 anotherPerson.job = 'teacher';
另外,如果我們想要自己實現原型模式,而不是使用封裝好的Object.create()函數,那么可以使用原型繼承來實現:
function F(){} F.prototype.g = function(){} //G類繼承F類 function G(){ F.call(this); } //原型繼承 function Fn(){}; Fn.prototype = F.prototype; G.prototype = new Fn(); G.prototype.constructor = G;
原型模式就是創建一個指定原型的對象。如果我們需要重復創建某個對象,那么就可以使用原型模式來實現。
結構型模式
1. 裝飾器模式
裝飾器模式:為對象添加新功能,不改變其原有的結構和功能。
適配器模式是原有的不能用了,要重新封裝接口。裝飾器模式是原有的還能用,但是需要新增一些東西來完善這個功能。
比如手機殼,手機本身的功能不受影響,手機殼就是手機的裝飾器模式。
class Circle { draw() { console.log('畫一個圓形'); } } class Decorator { constructor(circle) { this.circle = circle; } draw() { this.circle.draw(); this.setRedBorder(circle); } setRedBorder(circle) { console.log('設置紅色邊框') } } // 測試 let circle = new Circle(); let client = new Decorator(circle); client.draw();
輸出結果:
畫一個圓形 設置紅色邊框
如今都2021了,es7也應用廣泛,我們在es7中這么寫(ES7裝飾器):
1、安裝?yarn?add?babel-plugin-transform-decorators-legacy
2、新建.babelrc文件,進行下面的配置
{ "presets": ["es2015", "latest"], "plugins": ["transform-decorators-legacy"] }
3、上代碼
@testDec class Demo { // ... } function testDec(target) { target.isDec = true } console.log(Demo.isDec) //輸出true
打印出來了true,說明@testDec這個裝飾器已經成功了,函數是個裝飾器,用@testDec給Demo裝飾了一遍。這個target其實就是class?Demo,然后給她加一個isDec。
拆解后就是下面的內容:
// 裝飾器原理 @decorator class A {} // 等同于 class A {} A = decorator(A) || A;
裝飾器參數的形式
@testDec(false) class Demo { } function testDec(isDec) { return function (target) { target.isDec = isDec } } console.log(Demo.isDec);
驗證是否是一個真正的裝飾器模式需要驗證以下幾點:
1.將現有對戲那個和裝飾器進行分離,兩者獨立存在 2.符合開放封閉原則
2. 適配器模式
適配器模式:舊接口格式和使用者不兼容,中間加一個適配轉換接口。
比如國外的插座跟國內的插座不一樣,我們需要買個轉換器去兼容。
上代碼:
class Adaptee { specificRequest() { return '德國標準的插頭'; } } class Target { constructor() { this.adaptee = new Adaptee(); } request() { let info = this.adaptee.specificRequest(); return `${info} -> 轉換器 -> 中國標準的插頭` } } // 測試 let client = new Target(); client.request();
結果:
德國標準的插頭 -> 轉換器 -> 中國標準的插頭
場景上可封裝舊接口:
// 自己封裝的ajax,使用方式如下: ajax({ url: '/getData', type: 'Post', dataType: 'json', data: { id: '123' } }).done(function(){ }) // 但因為歷史原因,代碼中全都是: // $.ajax({...})
這個時候需要一個適配器:
// 做一層適配器 var $ = { ajax: function (options) { return ajax(options) } }
3. 代理模式
代理模式:使用者無權訪問目標對象,中間加代理,通過代理做授權和控制。
明星經紀人:比如有個演出,要請明星,要先聯系經紀人。
或者理解為:為一個對象提供一個代用品或者占位符,以便控制對它的訪問。例如圖片懶加載、中介等。
/** * pre:代理模式 * 小明追求A,B是A的好朋友,小明比不知道A什么時候心情好,不好意思直接將花交給A, * 于是小明將花交給B,再由B交給A. */ // 花的類 class Flower{ constructor(name){ this.name = name } } // 小明擁有sendFlower的方法 let Xioaming = { sendFlower(target){ var flower = new Flower("玫瑰花") target.receive(flower) } } // B對象中擁有接受花的方法,同時接收到花之后,監聽A的心情,并且傳入A心情好的時候函數 let B = { receive(flower){ this.flower =flower A.listenMood(()=>{ A.receive(this.flower) }) } } // A接收到花之后輸出花的名字 let A = { receive(flower){ console.log(`A收到了${flower.name} `) // A收到了玫瑰花 }, listenMood(func){ setTimeout(func,1000) } } Xioaming.sendFlower(B)
虛擬代理用于圖片的預加載
圖片很大,頁面加載時會空白,體驗不好,所以我們需要個占位符,來短暫替代這個圖片,等圖片加載好了放上去。
let myImage = (function(){ let img = new Image document.body.appendChild(img) return { setSrc:(src)=>{ img.src = src } } })() let imgProxy =(function(){ let imgProxy = new Image // 這個地方我使用了setTimeout來增強演示效果,否則本地加載太快,根本看不到。 imgProxy.onload=function(){ setTimeout(()=>{ myImage.setSrc(this.src) },2000) } return (src)=>{ myImage.setSrc("../../img/bgimg.jpeg") imgProxy.src=src } })() imgProxy("../../img/background-cover.jpg")
ES6?Proxy
其實在ES6中,已經有了Proxy,這個內置的函數。我們來用一個例子來演示一下他的用法。這是一個明星代理的問題。
let star={ name : "張XX", age:25, phone : "1300001111" } let agent = new Proxy(star, { get:function(target,key){ if(key === "phone"){ return "18839552597" }else if(key === "name"){ return "張XX" }else if(key === "price"){ return "12W" }else if(key === "customPrice"){ return target.customPrice } }, set:function(target,key,value){ if(key === "customPrice"){ if(value < "10"){ console.log("太低了!!!") return false }else{ target[key] = value return true } } } } ) console.log(agent.name) console.log(agent.price) console.log(agent.phone) console.log(agent.age) agent.customPrice = "12" console.log(agent) console.log(agent.customPrice)
設計原則驗證
代理類和目標類分離,隔離開目標類和使用者
符合開放封閉原則
行為型模式
1. 策略模式
策略模式是一種簡單卻常用的設計模式,它的應用場景非常廣泛。我們先了解下策略模式的概念,再通過代碼示例來更清晰的認識它。
策略模式由兩部分構成:一部分是封裝不同策略的策略組,另一部分是 Context。通過組合和委托來讓 Context 擁有執行策略的能力,從而實現可復用、可擴展和可維護,并且避免大量復制粘貼的工作。
策略模式的典型應用場景是表單校驗中,對于校驗規則的封裝。接下來我們就通過一個簡單的例子具體了解一下:
/** * 登錄控制器 */ function LoginController() { this.strategy = undefined; this.setStrategy = function (strategy) { this.strategy = strategy; this.login = this.strategy.login; } } /** * 用戶名、密碼登錄策略 */ function LocalStragegy() { this.login = ({ username, password }) => { console.log(username, password); // authenticating with username and password... } } /** * 手機號、驗證碼登錄策略 */ function PhoneStragety() { this.login = ({ phone, verifyCode }) => { console.log(phone, verifyCode); // authenticating with hone and verifyCode... } } /** * 第三方社交登錄策略 */ function SocialStragety() { this.login = ({ id, secret }) => { console.log(id, secret); // authenticating with id and secret... } } const loginController = new LoginController(); // 調用用戶名、密碼登錄接口,使用LocalStrategy app.use('/login/local', function (req, res) { loginController.setStrategy(new LocalStragegy()); loginController.login(req.body); }); // 調用手機、驗證碼登錄接口,使用PhoneStrategy app.use('/login/phone', function (req, res) { loginController.setStrategy(new PhoneStragety()); loginController.login(req.body); }); // 調用社交登錄接口,使用SocialStrategy app.use('/login/social', function (req, res) { loginController.setStrategy(new SocialStragety()); loginController.login(req.body); });
從以上示例可以得出使用策略模式有以下優勢:
方便在運行時切換算法和策略
代碼更簡潔,避免使用大量的條件判斷
關注分離,每個strategy類控制自己的算法邏輯,strategy和其使用者之間也相互獨立
2. 觀察者模式
觀察者模式又叫發布訂閱模式(Publish/Subscribe),它定義了一種一或一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。典型代表vue/react等。
使用觀察者模式的好處:
支持簡單的廣播通信,自動通知所有已經訂閱過的對象。
目標對象與觀察者存在的是動態關聯,增加了靈活性。
目標對象與觀察者之間的抽象耦合關系能夠單獨擴展以及重用。
當然給元素綁定事件的addEventListener()也是一種:
target.addEventListener(type, listener [, options]);
Target就是被觀察對象Subject,listener就是觀察者Observer。
觀察者模式中Subject對象一般需要實現以下API:
subscribe(): 接收一個觀察者observer對象,使其訂閱自己
unsubscribe(): 接收一個觀察者observer對象,使其取消訂閱自己
fire(): 觸發事件,通知到所有觀察者
用JavaScript手動實現觀察者模式:
// 被觀察者 function Subject() { this.observers = []; } Subject.prototype = { // 訂閱 subscribe: function (observer) { this.observers.push(observer); }, // 取消訂閱 unsubscribe: function (observerToRemove) { this.observers = this.observers.filter(observer => { return observer !== observerToRemove; }) }, // 事件觸發 fire: function () { this.observers.forEach(observer => { observer.call(); }); } }
驗證一下訂閱是否成功:
const subject = new Subject(); function observer1() { console.log('Observer 1 Firing!'); } function observer2() { console.log('Observer 2 Firing!'); } subject.subscribe(observer1); subject.subscribe(observer2); subject.fire();
輸出:
Observer 1 Firing! Observer 2 Firing!
驗證一下取消訂閱是否成功:
subject.unsubscribe(observer2); subject.fire();
輸出:
Observer 1 Firing!
3. 迭代器模式
ES6中的迭代器?Iterator?相信大家都不陌生,迭代器用于遍歷容器(集合)并訪問容器中的元素,而且無論容器的數據結構是什么(Array、Set、Map等),迭代器的接口都應該是一樣的,都需要遵循?迭代器協議。
迭代器模式解決了以下問題:
提供一致的遍歷各種數據結構的方式,而不用了解數據的內部結構
提供遍歷容器(集合)的能力而無需改變容器的接口
一個迭代器通常需要實現以下接口:
hasNext():判斷迭代是否結束,返回Boolean
next():查找并返回下一個元素
為Javascript的數組實現一個迭代器可以這么寫:
const item = [1, 'red', false, 3.14]; function Iterator(items) { this.items = items; this.index = 0; } Iterator.prototype = { hasNext: function () { return this.index < this.items.length; }, next: function () { return this.items[this.index++]; } }
驗證一下迭代器:
const iterator = new Iterator(item); while(iterator.hasNext()){ console.log(iterator.next()); }
輸出:
1, red, false, 3.14
ES6提供了更簡單的迭代循環語法?for...of,使用該語法的前提是操作對象需要實現?可迭代協議(The iterable protocol),簡單說就是該對象有個Key為?Symbol.iterator?的方法,該方法返回一個iterator對象。
比如我們實現一個?Range?類用于在某個數字區間進行迭代:
function Range(start, end) { return { [Symbol.iterator]: function () { return { next() { if (start < end) { return { value: start++, done: false }; } return { done: true, value: end }; } } } } }
驗證:
for (num of Range(1, 5)) { console.log(num); }
結果:
1, 2, 3, 4
4. 狀態模式
狀態模式:一個對象有狀態變化,每次狀態變化都會觸發一個邏輯,不能總是用if...else來控制。
比如紅綠燈:
// 狀態(紅燈,綠燈 黃燈) class State { constructor(color) { this.color = color; } // 設置狀態 handle(context) { console.log(`turn to ${this.color} light`); context.setState(this) } } // 主體 class Context { constructor() { this.state = null; } // 獲取狀態 getState() { return this.state; } setState(state) { this.state = state; } } // 測試 let context = new Context(); let green = new State('green'); let yellow = new State('yellow'); let red = new State('red'); // 綠燈亮了 green.handle(context); console.log(context.getState()) // 黃燈亮了 yellow.handle(context); console.log(context.getState()) // 紅燈亮了 red.handle(context); console.log(context.getState())
設計原則驗證
將狀態對象和主體對象分離,狀態的變化邏輯單獨處理
符合開放封閉原則
web前端 交通智能體
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。