原文地址:Understanding Design Patterns in JavaScriptjavascript
原文做者:Sukhjinder Arorajava
譯者:HelloGitHub-Robertes6
當啓動一個新的項目時候,咱們不該該立刻開始編程。而是首先應該定義項目的目的和範圍,而後列出其功能或規格。若是你已經開始編程或者正在從事一個複雜的項目,則應該選擇一個最適合你項目的設計模式。編程
在軟件工程中,設計模式是針對軟件設計中常見問題的可重用解決方案。設計模式也是經驗豐富的開發人員針對特定問題的最佳實踐。它能夠被看成編程的模板。設計模式
許多工程師要麼認爲設計模式浪費時間,要麼不知道如何恰當的使用設計模式。但若是能正確使用設計模式,則能夠幫助你寫出更好的可讀性更高的代碼,而且代碼更容易被維護和理解。閉包
最重要的是,設計模式爲軟件開發人員提供了通用的詞彙表。它們能讓學習你代碼的人很快了解代碼的意圖。例如,若是你的項目中使用了裝飾器模式,那麼新的開發能夠很快就知道這段代碼的做用,從而他們能夠將更多精力放在解決業務問題上,而不是試圖理解代碼在作什麼。編程語言
咱們已經知道了什麼是設計模式和它的重要性,下面咱們深刻研究一下 JavaScript 中的 7 種設計模式。ide
模塊是一段獨立的代碼,所以咱們能夠更新模塊而不會影響代碼的其它部分。模塊還容許咱們經過爲變量建立單獨的做用域來避免命名空間污染。當它們與其它代碼解耦時,咱們還能夠在其它項目中重用模塊。函數
模塊是任何現代 JavaScript 應用程序不可或缺的一部分,有助於保持代碼乾淨,獨立和有條理。在 JavaScript 中有許多方法能夠建立模塊,其中一種是模塊模式。學習
與其它編程語言不一樣,JavaScript 沒有訪問修飾符,也就是說,你不能將變量聲明爲私有的或公共的。所以,模塊模式也可用來模擬封裝的概念。
模塊模式使用 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()
例如:
// Prints '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(); } /** reveal methods and variables by assigning them to object properties */ return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName('Mark'); // prints Name: Mark myRevealingModule.getName();
這種模式讓咱們更容易知道哪些函數和變量是公共的,無形中提升了代碼的可讀性。執行代碼後 myRevealingModule
看起來像下所示:
const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };
當咱們調用 myRevealingModule.setName('Mark')
時,實際調用了內部的 publicSetName
。當調用 myRevealingModule.getName()
時,實際調用了內部的 publicGetName
例如:
myRevealingModule.setName('Mark'); // prints Name: Mark myRevealingModule.getName();
與模塊模式相比,揭示模塊模式的優點有:
在 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; } // This is a private function 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; } // This is a private function function privateLog() { console.log('Private Function'); } export {multiply, divide};
與導出模塊類似,有兩種使用 import
關鍵字導入模塊的方法。例如:
// main.js // importing multiple items import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7));
// main.js // importing all of module 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 中的對象是經過引用而不是經過值傳遞的。所以,內存中只有一個對象。例如:
// prints 'Mark' console.log(user.name); // prints 'Mark' console.log(user1.name); // prints 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(); // prints true console.log(user1 === user2);
調用此構造函數時,它將檢查 instance
對象是否存在。若是對象不存在,則將 this
變量分配給 instance
變量。若是該對象存在,則只返回該對象。
單例也可使用模塊模式來實現。例如:
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(); // prints 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' }); // Prints Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Prints Truck {doors: 2, state: "used", color: "white"} console.log(truck);
我爲類 VehicleFactory
建立了一個新的 factory
對象。而後,咱們經過調用 factory.createVehicle
方法而且傳遞 options
對象,其 vehicleType
屬性可能爲 car
或者 truck
來建立新 Car
或 Truck
對象。
裝飾器模式用於擴展對象的功能,而無需修改現有的類或構造函數。此模式可用於將特徵添加到對象中,而無需修改底層的代碼。
此模式的一個簡單示例爲:
function Car(name) { this.name = name; // Default values this.color = 'White'; } // Creating a new Object to decorate const tesla= new Car('Tesla Model 3'); // Decorating the object with new functionality tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); // prints black console.log(tesla.color);
這種模式的一個更實際的例子是:
假設汽車的成本取決於其功能的數量。若是沒有裝飾器模式,咱們將不得不爲不一樣的功能組合建立不一樣的類,每一個類都有一個 cost
方法來計算成本。例如:
class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }
可是,經過裝飾器模式,咱們能夠建立一個基類 car
而且經過裝飾器函數給不一樣的對象添加對應的成本邏輯。
class Car { constructor() { // Default Cost this.cost = function() { return 20000; } } } // Decorator function function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // Decorator function function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // Decorator function function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }
首先,咱們建立了小轎車的基類 Car
。而後針對要添加的特性建立了裝飾器而且此裝飾器以 Car
對象爲參數。而後經過返回更新後的小汽車成原本覆蓋對象的成本函數,且添加了一個用來標識某個特性是否已經被添加的屬性。
要添加新的功能,咱們只須要像下面同樣就能夠:
const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car);
最後,咱們能夠像這樣計算汽車的成本:
// Calculating total cost of the car console.log(car.cost());
咱們已經瞭解了 JavaScript 中使用的各類設計模式,可是這裏沒有涉及到能夠用 JavaScript 實現的設計模式。
儘管瞭解各類設計模式很重要,但不要過分使用它們也一樣重要。在使用設計模式以前,你應該仔細考慮你的問題是否適合該設計模式。要知道某個模式是否適合你的問題,應該好好研究該設計模式以及它的應用。
關注 HelloGitHub 公衆號 收到第一時間的更新。
還有更多開源項目的介紹和寶藏項目等待你的發掘。