使用適當的設計模式可以幫助你編寫更好、更易於理解的代碼。這樣的代碼也更容易維護。但是,重要的是不要過度使用它們。在使用設計模式之前,你應該仔細考慮你的問題是否符合設計模式。
當你開始一個新的項目時,你不會立即開始編碼。你必須定義項目的目的和範圍,然後列出項目特性或規格說明。之後,你可以開始編寫代碼,或者,如果你正在參與的是一個更復雜的項目,那麼你應該選擇一個最適合項目的設計模式。
在軟件工程中,設計模式是軟件設計中常見問題的可重用解決方案。設計模式代表了經驗豐富的軟件開發人員所使用的最佳實踐。設計模式可以看作是編程模板。
有許多程序員,他們要麼認爲設計模式浪費時間,要麼不知道如何恰當地應用它們。但是,使用適當的設計模式可以幫助你編寫更好、更易於理解的代碼。這樣的代碼也更容易維護。
最重要的是,設計模式爲軟件開發人員提供了一個可以談論的通用詞彙表。它們可以讓學習代碼的人快速瞭解代碼的意圖。
例如,如果你在項目中使用了裝飾模式,那麼新程序員就會立即知道那段代碼在做什麼,他們可以把更多的精力放在解決業務問題上,而不是試圖理解那段代碼在做什麼。
現在我們知道了什麼是設計模式,以及爲什麼它們很重要。接下來,讓我們深入探討下應用於 JavaScript 的各種設計模式。
模塊是一段自包含的代碼,因此,我們可以在不影響代碼其他部分的情況下更新模塊。模塊還允許我們通過爲變量創建單獨的作用域來避免命名空間污染。當模塊與其他代碼片段鬆耦合時,我們還可以在其他項目中重用它們。
模塊是任何現代化 JavaScript 應用程序的組成部分,有助於保持代碼的整潔、隔離和條理性。使用 JavaScript 創建模塊有很多方法,其中之一就是模塊模式。
像 Bit 這樣的平臺可以幫助你將模塊和組件轉換成共享的構建塊,可以在任何項目中共享、發現和開發。不需要任何重構,就可以使用一種快速且可擴展的方式共享和重用代碼。
與其他編程語言不同,JavaScript 沒有訪問修飾符,也就是說,不能將變量聲明爲 private 或 public。因此,模塊模式也被用來模擬封裝的概念。
該模式使用 iife(即時調用函數表達式)、閉包和函數作用域來模擬這個概念,例如:
const myModule = (function() { const privateVariable = 'Hello World'; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();
由於是 iife,所以代碼會立即執行,返回的對象賦給 myModule 變量。由於是閉包,所以返回的對象仍然可以訪問在 iife 中定義的函數和變量,即使在 iife 結束之後。
因此,在 iife 中定義的變量和函數本質上是對外部作用域隱藏的,這使得它成爲 myModule 變量私有的。
執行代碼後,myModule 變量如下:
const myModule = { publicMethod: function() { privateMethod(); }};
因此,我們可以調用 publicMethod(),而它又會調用 privateMethod(),例如:
// 打印'Hello World' module.publicMethod();
揭示模塊模式是經 Christian Heilmann 略微改進的模塊模式。模塊模式的問題是,我們必須創建新的公共函數來調用私有函數和變量。
在這個模式中,我們將把返回對象的屬性映射到我們想要公開的私有函數。這就是爲什麼它被稱爲揭示模塊模式,例如:
const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar = 'Hello World'; function privateFunction() { console.log('Name: '+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } /** 把希望公開的方法和變量賦給對象屬性 */ return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName('Mark'); // 打印姓名:Mark myRevealingModule.getName();
這種模式使我們更容易理解哪些函數和變量可以公開訪問,這有助於提高代碼的可讀性。
執行代碼之後,myRevealingModule 是下面這個樣子:
const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };
我們可以調用 myrevealingmodule. setname ('Mark'),它是對方法 publicSetName 的引用,而 myRevealingModule.getName() 是對內部方法 publicGetName 的引用,例如:
myRevealingModule.setName('Mark'); // 打印姓名: Mark myRevealingModule.getName();
與模塊模式相比,揭示模塊模式的優點如下
通過修改 return 語句中的一行代碼,我們就可以將成員從 public 更改爲 private,反之亦然。
返回的對象不包含任何函數定義,所有右側表達式都在 iife 中定義,這使得代碼清晰且易於閱讀。
在 ES6 之前,JavaScript 沒有內置模塊,因此,開發人員不得不依賴第三方庫或模塊模式來實現模塊。但是在 ES6 中,JavaScript 有了本地模塊。
ES6 模塊存儲在文件中。每個文件只能有一個模塊。默認情況下,模塊中的所有內容都是私有的。函數、變量和類都是使用 export 關鍵字公開的。模塊內的代碼總是在嚴格模式下運行。
有多種方法可以公開函數和變量聲明:
在函數和變量聲明前添加 export 關鍵字,例如:
// utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } // 這是一個私有函數 function privateLog() { console.log('Private Function'); }
在代碼末尾添加 export 關鍵字,其中包含我們希望公開的函數名和變量名,例如:
// utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } // 這是一個私有函數 function privateLog() { console.log('Private Function'); } export {multiply, divide};
導入模塊
和輸出模塊類似,藉助 import,有多種方法可以導入模塊:
一次導入多個項:
// main.js // 導入多個項 import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7));
導入所有模塊:
// main.js // 導入所有模塊 import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));
導入和輸出的別名
如果你想要避免命名衝突,則可以在輸出和導入時更改名稱,例如:
重命名輸出:
// utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply};
重命名導入:
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7));
單例對象是隻能實例化一次的對象。如果一個類的實例不存在,單例模式就會創建一個新的類實例。如果實例存在,它只返回對該對象的引用。對構造函數的任何重複調用都會獲取相同的對象。
JavaScript 語言一直都內置了的單例,只是我們不把它們叫做單例,我們稱它們爲對象字面量,例如:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } };
因爲 JavaScript 中的每個對象都佔用一個唯一的內存位置,當我們調用 user 對象時,我們本質上是返回了該對象的引用。
如果我們試圖將 user 變量複製到另一個變量中並修改該變量,例如:
const user1 = user; user1.name = 'Mark';
我們會看到,兩個對象都被修改了,因爲在 JavaScript 中,對象是通過引用傳遞的,而不是值。因此,內存中只有一個對象,例如:
// 打印'Mark' console.log(user.name); // 打印'Mark' console.log(user1.name); // 打印 true console.log(user === user1);
單例模式可以使用構造函數實現,例如:
let instance = null; function User() { if(instance) { return instance; } instance = this; this.name = 'Peter'; this.age = 25; return instance; } const user1 = new User(); const user2 = new User(); // 打印 true console.log(user1 === user2);
當調用這個構造函數時,它會檢查實例對象是否存在。如果對象不存在,它就將這個變量賦給實例變量。如果對象存在,它只返回那個對象。
單例也可以使用模塊模式實現,例如:
const singleton = (function() { let instance; function init() { return { name: 'Peter', age: 24, }; } return { getInstance: function() { if(!instance) { instance = init(); } return instance; } } })(); const instanceA = singleton.getInstance(); const instanceB = singleton.getInstance(); // 打印 true console.log(instanceA === instanceB);
在上面的代碼中,我們通過調用 singleton.getInstance 方法來創建一個新實例。如果實例已經存在,則該方法只是返回這個實例,如果實例不存在,則調用 init() 函數創建一個新的實例。
工廠模式使用工廠方法創建對象,而不指定所創建對象的確切類或構造函數。
工廠模式用於創建對象,而不公開實例化邏輯。當我們需要根據特定條件生成不同的對象時,可以使用此模式,例如:
class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'brand new'; this.color = options.color || 'white'; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'used'; this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === 'car') { return new Car(options); } else if(options.vehicleType === 'truck') { return new Truck(options); } } }
這裏,我創建了一個 Car 類和一個 Truck 類(帶有一些默認值),用於創建新的 Car 和 Truck 對象。我還定義了一個 VehicleFactory 類,基於 options 對象中接收到的 vehicleType 屬性創建和返回一個新的對象。
const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); // 打印 Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // 打印 Truck {doors: 2, state: "used", color: "white"} console.log(truck);
我已經創建了一個新的 VehicleFactory 類的對象工廠。之後,我們可以調用 factory.createVehicle 方法,傳入一個 vehicleType 屬性值爲 car 或 truck 的 options 對象。
裝飾模式用於擴展對象的功能,而不修改現有的類或構造函數。該模式可用於向對象添加特性,而不修改使用它們的底層代碼。
下面是這個模式的一個簡單例子:
function Car(name) { this.name = name; // 默認值 this.color = 'White'; } // 新建一個需要裝飾的對象 const tesla= new Car('Tesla Model 3'); // 使用新功能裝飾對象 tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); // 打印 black console.log(tesla.color);
對於這種模式,一個更實際的例子是,比方說,一輛車的價格取決於它有多少功能。如果沒有裝飾模式,我們將不得不爲不同的特性組合創建不同的類,每個類都有計算成本的 cost 方法,例如:
class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }
但是使用裝飾模式,我們可以創建一個基類 Car,並使用裝飾函數將不同配置的成本添加到它的對象中,例如:
class Car { constructor() { // 默認值 this.cost = function() { return 20000; } } } // 裝飾函數 function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // 裝飾函數 function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // 裝飾函數 function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }
首先,我們創建一個創建 Car 對象的基類 Car。然後,爲要添加的功能創建裝飾,並將 Car 對象作爲參數傳遞。然後,我們重寫這個對象的 cost 函數,該函數返回更新後的汽車成本,並向該對象添加一個新屬性,表明添加了哪些功能。
要添加新功能,我們可以這樣做:
const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car);
最後,我們可以像下面這樣計算汽車的成本:
// 計算汽車的總成本 console.log(car.cost());
我們已經瞭解了 JavaScript 中使用的各種設計模式,但還有一些可以用 JavaScript 實現的設計模式我在這裏沒有涉及。
雖然瞭解各種設計模式很重要,但同樣重要的是不要過度使用它們。在使用設計模式之前,你應該仔細考慮你的問題是否符合設計模式。要知道一個模式是否適合你的問題,你應該研究設計模式以及該設計模式的應用。
本次給大家推薦一個免費的學習羣,裏面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q羣:943129070,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視頻資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。