文地址:Understanding Design Patterns in JavaScript
原文作者:Sukhjinder Arora
譯者:HelloGitHub-Robert
當啟動一個新的項目時候,我們不應該馬上開始編程。而是首先應該定義項目的目的和范圍,然后列出其功能或規格。如果你已經開始編程或者正在從事一個復雜的項目,則應該選擇一個最適合你項目的設計模式。
在軟件工程中,設計模式是針對軟件設計中常見問題的可重用解決方案。設計模式也是經驗豐富的開發人員針對特定問題的最佳實踐。它可以被當作編程的模板。
許多工程師要么認為設計模式浪費時間,要么不知道如何恰當的使用設計模式。但如果能正確使用設計模式,則可以幫助你寫出更好的可讀性更高的代碼,并且代碼更容易被維護和理解。
最重要的是,設計模式為軟件開發人員提供了通用的詞匯表。它們能讓學習你代碼的人很快了解代碼的意圖。例如,如果你的項目中使用了裝飾器模式,那么新的開發可以很快就知道這段代碼的作用,從而他們可以將更多精力放在解決業務問題上,而不是試圖理解代碼在做什么。
我們已經知道了什么是設計模式和它的重要性,下面我們深入研究一下 JavaScript 中的 7 種設計模式。
模塊是一段獨立的代碼,因此我們可以更新模塊而不會影響代碼的其它部分。模塊還允許我們通過為變量創建單獨的作用域來避免命名空間污染。當它們與其它代碼解耦時,我們還可以在其它項目中重用模塊。
模塊是任何現代 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 關鍵字來暴露函數、變量和類。模塊內的代碼始終在嚴格模式下運行。
有兩種方法可以導出函數和變量聲明:
// 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');
}
// 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 的「譯文亦舞」系列,讓你的才華舞動起來!把優秀的文章分享給更多的人。要求:
關注 HelloGitHub 頭條號第一時間收到推送
應性本質上是關于系統如何對數據變化作出反應,有不同類型的響應性。然而,在這篇文章中,我們關注的是響應性,即響應數據變化而采取行動。
作為一名前端開發者,Pavel Pogosov 每天都要面對這個問題。因為瀏覽器本身是一個完全異步的環境。現代 Web 界面必須快速響應用戶的操作,這包括更新 UI、發送網絡請求、管理導航和執行各種其他任務。
盡管人們常常將響應性與框架聯系在一起,Pavel Pogosov 認為通過純 JavaScript 實現響應性可以學到很多。所以,我們將自己編寫一些模式代碼,并研究一些基于響應性的原生瀏覽器 API。
class PubSub {
constructor() {
this.subscribers={};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event]=[];
}
this.subscribers[event].push(callback);
}
// 向特定事件的所有訂閱者發布消息
publish(event, data) {
if (this.subscribers[event]) {
this.subscribers[event].forEach((callback)=> {
callback(data);
});
}
}
}
const pubsub=new PubSub();
pubsub.subscribe('news', (message)=> {
console.log(`訂閱者1收到了新聞:${message}`);
});
pubsub.subscribe('news', (message)=> {
console.log(`訂閱者2收到了新聞:${message}`);
});
// 向 'news' 事件發布消息
pubsub.publish('news', '最新頭條新聞:...');
// 控制臺日志輸出:
// 訂閱者1收到了新聞:最新頭條新聞:...
// 訂閱者2收到了新聞:最新頭條新聞:...
一個常見的使用示例是 Redux。這款流行的狀態管理庫基于這種模式(或更具體地說,是 Flux 架構)。在 Redux 的上下文中,工作機制相當簡單:
發布者:store 充當發布者。當一個 action 被派發時,store 會通知所有訂閱的組件狀態的變化。 訂閱者:應用程序中的 UI 組件是訂閱者。它們訂閱 Redux store 并在狀態變化時接收更新。
瀏覽器通過 CustomEvent 類和 dispatchEvent 方法提供了一個用于觸發和訂閱自定義事件的 API。后者不僅能讓我們觸發事件,還能附加任何想要的數據。
const customEvent=new CustomEvent('customEvent', {
detail: '自定義事件數據', // 將所需數據附加到事件
});
const element=document.getElementById('.element-to-trigger-events');
element.addEventListener('customEvent', (event)=> {
console.log(`訂閱者1收到了自定義事件:${event.detail}`);
});
element.addEventListener('customEvent', (event)=> {
console.log(`訂閱者2收到了自定義事件:${event.detail}`);
});
// 觸發自定義事件
element.dispatchEvent(customEvent);
// 控制臺日志輸出:
// 訂閱者1收到了自定義事件:自定義事件數據
// 訂閱者2收到了自定義事件:自定義事件數據
如果你不想在全局 window 對象上分派事件,可以創建你自己的事件目標。
通過擴展原生 EventTarget 類,你可以向其新實例分派事件。這確保你的事件僅在新類本身上觸發,避免了全局傳播。此外,你可以直接將處理程序附加到這個特定實例上。
class CustomEventTarget extends EventTarget {
constructor() {
super();
}
// 觸發自定義事件的自定義方法
triggerCustomEvent(eventName, eventData) {
const event=new CustomEvent(eventName, { detail: eventData });
this.dispatchEvent(event);
}
}
const customTarget=new CustomEventTarget();
// 向自定義事件目標添加事件監聽器
customTarget.addEventListener('customEvent', (event)=> {
console.log(`自定義事件收到了數據:${event.detail}`);
});
// 觸發自定義事件
customTarget.triggerCustomEvent('customEvent', '你好,自定義事件!');
// 控制臺日志輸出:
// 自定義事件收到了數據:你好,自定義事件!
觀察者模式與 PubSub 非常相似。你訂閱 Subject,然后它通知其訂閱者(觀察者)關于變化,使他們能夠做出相應的反應。這種模式在構建解耦和靈活的架構中發揮了重要作用。
class Subject {
constructor() {
this.observers=[];
}
addObserver(observer) {
this.observers.push(observer);
}
// 從列表中移除觀察者
removeObserver(observer) {
const index=this.observers.indexOf(observer);
if (index !==-1) {
this.observers.splice(index, 1);
}
}
// 通知所有觀察者關于變化
notify() {
this.observers.forEach((observer)=> {
observer.update();
});
}
}
class Observer {
constructor(name) {
this.name=name;
}
// 通知時調用的更新方法
update() {
console.log(`${this.name} 收到了更新。`);
}
}
const subject=new Subject();
const observer1=new Observer('觀察者1');
const observer2=new Observer('觀察者2');
// 將觀察者添加到主體
subject.addObserver(observer1);
subject.addObserver(observer2);
// 通知觀察者關于變化
subject.notify();
// 控制臺日志輸出:
// 觀察者1 收到了更新。
// 觀察者2 收到了更新。
如果你想對對象的變化做出反應,Proxy 是一個好方法。它讓我們在設置或獲取對象字段的值時實現響應性。
const person={
name: 'Pavel',
age: 22,
};
const reactivePerson=new Proxy(person, {
// 攔截設置操作
set(target, key, value) {
console.log(`將 ${key} 設置為 ${value}`);
target[key]=value;
// 表示設置值是否成功
return true;
},
// 攔截獲取操作
get(target, key) {
console.log(`獲取 ${key}`);
return target[key];
},
});
reactivePerson.name='Sergei'; // 將 name 設置為 Sergei
console.log(reactivePerson.name); // 獲取 name: Sergei
reactivePerson.age=23; // 將 age 設置為 23
console.log(reactivePerson.age); // 獲取 age: 23
如果你不需要跟蹤對象中的所有字段,可以使用 Object.defineProperty 或一組 Object.defineProperties 來選擇特定的一個或幾個。
const person={
_originalName: 'Pavel', // 私有屬性
}
Object.defineProperty(person, 'name', {
get() {
console.log('獲取屬性 name')
return this._originalName
},
set(value) {
console.log(`將屬性 name 設置為值 ${value}`)
this._originalName=value
},
})
console.log(person.name) // '獲取屬性 name' 和 'Pavel'
person.name='Sergei' // 將屬性 name 設置為值 Sergei
在 DOM 中實現響應性的一種方法是使用 MutationObserver。其 API 允許我們觀察目標元素及其子元素的屬性變化和文本內容變化。
function handleMutations(mutationsList, observer) {
mutationsList.forEach((mutation)=> {
// 觀察到的元素的一個屬性發生了變化
if (mutation.type==='attributes') {
console.log(`屬性 '${mutation.attributeName}' 更改為 '${mutation.target.getAttribute(mutation.attributeName)}'`);
}
});
}
const observer=new MutationObserver(handleMutations);
const targetElement=document.querySelector('.element-to-observe');
// 開始觀察目標元素
observer.observe(targetElement, { attributes: true });
IntersectionObserver API 允許對目標元素與另一個元素或視口區域的交集做出反應。
function handleIntersection(entries, observer) {
entries.forEach((entry)=> {
// 目標元素在視口中
if (entry.isIntersecting) {
entry.target.classList.add('visible');
} else {
entry.target.classList.remove('visible');
}
});
}
const observer=new IntersectionObserver(handleIntersection);
const targetElement=document.querySelector('.element-to-observe');
// 開始觀察目標元素
observer.observe(targetElement);
感謝閱讀!
計模式太多了,貌似有23種,其實我們在平時的工作中沒有必要特意去用什么樣的設計模式,或者你在不經意間就已經用了設計模式當中的一種。本文旨在總結平時相對來說用的比較多的設計模式。
什么是設計模式
百度百科:
設計模式(Design pattern)是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。
使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式于己于他人于系統都是多贏的;設計模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。
實際情況:
設計模式絕對不是紙上談兵的知識,光看書就以為自己懂了,那只是井底之蛙之見,設計模式絕對是從實踐中來到實踐中去的!如果編碼經驗很少,也不太可能能理解好設計模式,但凡軟件設計能力強的人編碼功底都是相當扎實的。
如果沒有能深刻理解面向對象,也不太可能理解好設計模式,剛剛畢業或者才工作一兩年就說自己面向對象能力強的人,基本上就是夸夸其談的人。
很明顯,我就是屬于那種夸夸其談的人,哈哈,不過希望對本文的總結,讓自己更加了解這些設計模式,理解的更加透徹。
單體模式:
概念:
單體是一個用來劃分命名空間并將一批相關的屬性和方法組織在一起的對象,如果他可以被實例化,那么他只能被實例化一次。
特點:
可以來劃分命名空間,從而清除全局變量所帶來的危險。
利用分支技術來來封裝瀏覽器之間的差異。
可以把代碼組織的更為一體,便于閱讀和維護。
代碼實現:
/*Basic Singleton*/var Singleton={
attribute:true,
method1:function(){},
method2:function(){}
};
應用場景:
單體模式在我們平時的應用中用的比較多的,相當于把我們的代碼封裝在一個起來,只是暴露一個入口,從而避免全部變量的污染。
工廠模式:
概念:
工廠模式的定義:提供創建對象的接口,意思就是根據領導(調用者)的指示(參數),生產相應的產品(對象)。
創建一個對象常常需要復雜的過程,所以不適合在一個復雜的對象中。
創建對象可能會導致大量的重復代碼,也可能提供不了足夠級別的抽象。
工廠就是把成員對象的創建工作轉交給一個外部對象,好處在于消除對象之間的耦合(也就是相互影響)
分類:
簡單工廠模式:使用一個類,通常為單體,來生成實例。
復雜工廠模式定義是:將其成員對象的實列化推到子類中,子類可以重寫父類接口方法以便創建的時候指定自己的對象類型。
父類只對創建過程中的一般性問題進行處理,這些處理會被子類繼承,子類之間是相互獨立的,具體的業務邏輯會放在子類中進行編寫。
代碼實現:
簡單工廠模式:
var XMLHttpFactory=function(){}; //這是一個簡單工廠模式 XMLHttpFactory.createXMLHttp=function(){
var XMLHttp=null;
if (window.XMLHttpRequest){
XMLHttp=new XMLHttpRequest()
}else if (window.ActiveXObject){
XMLHttp=new ActiveXObject("Microsoft.XMLHTTP")
}
return XMLHttp;
}
//XMLHttpFactory.createXMLHttp()這個方法根據當前環境的具體情況返回一個XHR對象。 var AjaxHander=function(){
var XMLHttp=XMLHttpFactory.createXMLHttp();
...
}
復雜工廠模式:流程==》 先設計一個抽象類,這個類不能被實例化,只能用來派生子類,最后通過對子類的擴展實現工廠方法
var XMLHttpFactory=function(){}; //這是一個抽象工廠模式XMLHttpFactory.prototype={
//如果真的要調用這個方法會拋出一個錯誤,它不能被實例化,只能用來派生子類 createFactory:function(){
throw new Error('This is an abstract class');
}
}var XHRHandler=function(){}; //定義一個子類// 子類繼承父類原型方法extend( XHRHandler , XMLHttpFactory );
XHRHandler.prototype=new XMLHttpFactory(); //把超類原型引用傳遞給子類,實現繼承XHRHandler.prototype.constructor=XHRHandler; //重置子類原型的構造器為子類自身//重新定義createFactory 方法XHRHandler.prototype.createFactory=function(){
var XMLHttp=null;
if (window.XMLHttpRequest){
XMLHttp=new XMLHttpRequest();
}else if (window.ActiveXObject){
XMLHttp=new ActiveXObject("Microsoft.XMLHTTP")
}
return XMLHttp;
}
應用場景:
以下幾種情景下工廠模式特別有用:
(1)對象的構建十分復雜
(2)需要依賴具體環境創建不同實例
(3)處理大量具有相同屬性的小對象
優點:
可以實現一些相同的方法,這些相同的方法我們可以放在父類中編寫代碼,那么需要實現具體的業務邏輯,那么可以放在子類中重寫該父類的方法,去實現自己的業務邏輯;
也就是說有兩點:
1、弱化對象間的耦合,防止代碼的重復。在一個方法中進行類的實例化,可以消除重復性的代碼。
2、重復性的代碼可以放在父類去編寫,子類繼承于父類的所有成員屬性和方法,子類只專注于實現自己的業務邏輯。
缺點:
當工廠增加到一定程度的時候,提升了代碼的復雜度,可讀性下降。而且沒有解決對象的識別問題,即怎么知道一個對象的類型。
單例模式
概念:
單例模式定義了一個對象的創建過程,此對象只有一個單獨的實例,并提供一個訪問它的全局訪問點。也可以說單例就是保證一個類只有一個實例,實現的方法一般是先判斷實例存在與否,如果存在直接返回,如果不存在就創建了再返回,這就確保了一個類只有一個實例對象。
代碼實現:
單例的實現有很多種,下面只介紹其中的一種,使用閉包方式來實現單例,代碼如下:
var single=(function(){ var unique; function getInstance(){
// 如果該實例存在,則直接返回,否則就對其實例化 if( unique===undefined ){ unique=new Construct(); } return unique; } function Construct(){ // ... 生成單例的構造函數的代碼 } return { getInstance : getInstance }})();
上面的代碼中,unique便是返回對象的引用,而 getInstance便是靜態方法獲得實例。Construct 便是創建實例的構造函數。
可以通過 single.getInstance() 來獲取到單例,并且每次調用均獲取到同一個單例。這就是 單例模式 所實現的效果。
使用場景:
單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如全局緩存、瀏覽器的window對象。在js開發中,單例模式的用途同樣非常廣泛。試想一下,當我們
單擊登錄按鈕的時候,頁面中會出現一個登錄框,而這個浮窗是唯一的,無論單擊多少次登錄按鈕,這個浮窗只會被創建一次。因此這個登錄浮窗就適合用單例模式。
總結一下它的使用場景:
1、可以用它來劃分命名空間
2、借助單例模式,可以把代碼組織的更為一致,方便閱讀與維護
觀察者模式(發布訂閱模式)
概念:
定義對象間的一種一對多的依賴關系,以便當一個對象的狀態發生改變時,所有依賴于它的對象都得到通知并自動刷新,也被稱為是發布訂閱模式。
它需要一種高級的抽象策略,以便訂閱者能夠彼此獨立地發生改變,而發行方能夠接受任何有消費意向的訂閱者。
應用場景:
這個模式要先說應用場景,比較好理解。
打一個離我們比較近的一個場景,博客園里面有一個訂閱的按鈕(貌似有bug),比如小A,小B,小C都訂閱了我的博客,當我的博客一有更新時,就會統一發布郵件給他們這三個人,就會通知這些訂閱者
發布訂閱模式的流程如下:
1. 確定誰是發布者(比如我的博客)。
2. 然后給發布者添加一個緩存列表,用于存放回調函數來通知訂閱者。
3. 發布消息,發布者需要遍歷這個緩存列表,依次觸發里面存放的訂閱者回調函數。
4、退訂(比如不想再接收到這些訂閱的信息了,就可以取消掉)
代碼如下:
var pubsub={}; // 定義發布者(function (q) { var list=[], //回調函數存放的數組,也就是記錄有多少人訂閱了我們東西
subUid=-1; // 發布消息,遍歷訂閱者
q.publish=function (type, content) { // type 為文章類型,content為文章內容
// 如果沒有人訂閱,直接返回
if (!list[type]) { return false;
}
setTimeout(function () { var subscribers=list[type],
len=subscribers ? subscribers.length : 0; while (len--) { // 將內容注入到訂閱者那里 subscribers[len].func(type, content);
}
}, 0); return true;
}; //訂閱方法,由訂閱者來執行
q.subscribe=function (type, func) { // 如果之前沒有訂閱過
if (!list[type]) {
list[type]=[];
} // token相當于訂閱者的id,這樣的話如果退訂,我們就可以針對它來知道是誰退訂了。
var token=(++subUid).toString(); // 每訂閱一個,就把它存入到我們的數組中去 list[type].push({
token: token,
func: func
}); return token;
}; //退訂方法
q.unsubscribe=function (token) { for (var m in list) { if (list[m]) { for (var i=0, j=list[m].length; i < j; i++) { if (list[m][i].token===token) {
list[m].splice(i, 1); return token;
}
}
}
} return false;
};
} (pubsub));//將訂閱賦值給一個變量,以便退訂var girlA=pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlA訂閱的'+type + ": 內容內容為:" + content);
});var girlB=pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlB訂閱的'+type + ": 內容內容為:" + content);
});var girlC=pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlC訂閱的'+type + ": 內容內容為:" + content);
});//發布通知pubsub.publish('js類的文章', '關于js的內容');
// 輸出:// girlC訂閱的js類的文章: 內容內容為:關于js的內容// test3.html:78 girlB訂閱的js類的文章: 內容內容為:關于js的內容// test3.html:75 girlA訂閱的js類的文章: 內容內容為:關于js的內容//girlA退訂了關于js類的文章 setTimeout(function () {
pubsub.unsubscribe(girlA);
}, 0);//再發布一次,驗證一下是否還能夠輸出信息pubsub.publish('js類的文章', "關于js的第二篇文章");// 輸出:// girlB訂閱的js類的文章: 內容內容為:關于js的第二篇文章// girlC訂閱的js類的文章: 內容內容為:關于js的第二篇文章
代碼可以自己運行一遍,這樣比較好理解
優缺點:
優點:當我們需要維護相關對象的一致性的時候,使用觀察者模式,,就可以避免對象之間的緊密耦合。例如,一個對象可以通知另外一個對象,而不需要知道這個對象的信息。
缺點:在發布/訂閱模式中,如果我們需要將發布者同訂閱者上解耦,將會在一些情況下,導致很難確保我們應用中的特定部分按照我們預期的那樣正常工作。也就是說它的優點也可能是它的缺點
策略模式
概念:
策略模式指的是定義一些列的算法,把他們一個個封裝起來,目的就是將算法的使用與算法的實現分離開來。說白了就是以前要很多判斷的寫法,現在把判斷里面的內容抽離開來,變成一個個小的個體。
代碼實現:
代碼情景為超市促銷,vip為5折,老客戶3折,普通顧客沒折,計算最后需要支付的金額。
沒有使用策略模式的情況:
function Price(personType, price) { //vip 5 折
if (personType=='vip') { return price * 0.5;
}
else if (personType=='old'){ //老客戶 3 折
return price * 0.3;
} else { return price; //其他都全價 }
}
不足之處:不好的地方,當我有其他方面的折扣時,又或者我活動的折扣時經常變化的,這樣就要不斷的修改if..else里面的條件了。而且也違背了設計模式的一個原則:對修改關閉,對擴展開放的原則;
使用策略模式之后:
// 對于vip客戶function vipPrice() { this.discount=0.5;
}
vipPrice.prototype.getPrice=function(price) {
return price * this.discount;
}// 對于老客戶function oldPrice() { this.discount=0.3;
}
oldPrice.prototype.getPrice=function(price) { return price * this.discount;
}// 對于普通客戶function Price() { this.discount=1;
}
Price.prototype.getPrice=function(price) { return price ;
}// 上下文,對于客戶端的使用function Context() { this.name=''; this.strategy=null; this.price=0;
}
Context.prototype.set=function(name, strategy, price) { this.name=name; this.strategy=strategy; this.price=price;
}
Context.prototype.getResult=function() {
console.log(this.name + ' 的結賬價為: ' + this.strategy.getPrice(this.price));
}var context=new Context();var vip=new vipPrice();
context.set ('vip客戶', vip, 200);
context.getResult(); // vip客戶 的結賬價為: 100var old=new oldPrice();
context.set ('老客戶', old, 200);
context.getResult(); // 老客戶 的結賬價為: 60var Price=new Price();
context.set ('普通客戶', Price, 200);
context.getResult(); // 普通客戶 的結賬價為: 200
通過策略模式,使得客戶的折扣與算法解藕,又使得修改跟擴展能獨立的進行,不影到客戶端或其他算法的使用;
使用場景:
策略模式最實用的場合就是某個“類”中包含有大量的條件性語句,比如if...else 或者 switch。每一個條件分支都會引起該“類”的特定行為以不同的方式作出改變。以其維
護一段龐大的條件性語句,不如將每一個行為劃分為多個獨立的對象。每一個對象被稱為一個策略。設置多個這種策略對象,可以改進我們的代碼質量,也更好的進行單元測試。
模板模式
概念:
定義了一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
通俗的講,就是將一些公共方法封裝到父類,子類可以繼承這個父類,并且可以在子類中重寫父類的方法,從而實現自己的業務邏輯。
代碼實現:
比如前端面試,基本包括筆試,技術面試,領導面試,HR面試等,但是每個公司的筆試題,技術面可能不一樣,也可能一樣,一樣的就繼承父類的方法,不一樣的就重寫父類的方法
var Interview=function(){};// 筆試Interview.prototype.writtenTest=function(){
console.log("這里是前端筆試題");
};// 技術面試Interview.prototype.technicalInterview=function(){
console.log("這里是技術面試");
};
// 領導面試Interview.prototype.leader=function(){
console.log("領導面試");
};// 領導面試Interview.prototype.HR=function(){
console.log("HR面試");
};// 等通知Interview.prototype.waitNotice=function(){
console.log("等通知啊,不知道過了沒有哦");
};// 代碼初始化Interview.prototype.init=function(){ this.writtenTest(); this.technicalInterview(); this.leader(); this.HR(); this.waitNotice();
};// 阿里巴巴的筆試和技術面不同,重寫父類方法,其他繼承父類方法。var AliInterview=function(){};
AliInterview.prototype=new Interview();// 子類重寫方法 實現自己的業務邏輯AliInterview.prototype.writtenTest=function(){
console.log("阿里的技術題就是難啊");
}
AliInterview.prototype.technicalInterview=function(){
console.log("阿里的技術面就是叼啊");
}var AliInterview=new AliInterview();
AliInterview.init();// 阿里的技術題就是難啊// 阿里的技術面就是叼啊// 領導面試// HR面試// 等通知啊,不知道過了沒有哦
應用場景:
模板模式主要應用在一些代碼剛開要一次性實現不變的部分。但是將來頁面有修改,需要更改業務邏輯的部分或者重新添加新業務的情況。主要是通過子類來改寫父類的情
況,其他不需要改變的部分繼承父類。
代理模式
概念:
代理模式的中文含義就是幫別人做事,javascript的解釋為:把對一個對象的訪問, 交給另一個代理對象來操作.
代碼實現:
比如我們公司的補打卡是最后是要交給大boss來審批的,但是公司那么多人,每天都那么多補打卡,那大boss豈不是被這些瑣事累死。所以大boss下會有一個助理,來幫
忙做這個審批,最后再將每個月的補打卡統一交給大boss看看就行。
// 補打卡事件var fillOut=function (lateDate) { this.lateDate=lateDate;};// 這是bigBossvar bigBoss=function (fillOut) { this.state=function (isSuccess) { console.log("忘記打卡的日期為:" + fillOut.lateDate + ", 補打卡狀態:" + isSuccess); }};// 助理代理大boss 完成補打卡審批var proxyAssis=function (fillOut) { this.state=function (isSuccess) { (new bigBoss(fillOut)).state(isSuccess); // 替bigBoss審批 }};// 調用方法:var proxyAssis=new proxyAssis(new fillOut("2016-9-11"));proxyAssis.state("補打卡成功");
// 忘記打卡的日期為:2016-9-11, 補打卡狀態:補打卡成功
應用場景:
比如圖片的懶加載,我們就可以運用這種技術。在圖片未加載完成之前,給個loading圖片,加載完成后再替換成實體路徑。
var myImage=(function(){ var imgNode=document.createElement("img");
document.body.appendChild(imgNode); return function(src){
imgNode.src=src;
}
})();// 代理模式var ProxyImage=(function(){ var img=new Image();
img.onload=function(){
myImage(this.src);
}; return function(src) { // 占位圖片loading
myImage("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
img.src=src;
}
})();// 調用方式ProxyImage("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png"); // 真實要展示的圖片
當然,這種懶加載方法不用代理模式也是可以實現的,只是用代理模式。我們可以讓 myImage 只做一件事,只負責將實際圖片加入到頁面中,而loading圖片交給ProxyImage去做。從而降低代碼的耦合度。因為當我不想用loading的時候,可以直接調用myImage 方法。也即是說假如我門不需要代理對象的話,直接可以換成本體對象調用該方法即可。
外觀模式
概念:
外觀模式是很常見。其實它就是通過編寫一個單獨的函數,來簡化對一個或多個更大型的,可能更為復雜的函數的訪問。也就是說可以視外觀模式為一種簡化某些內容的手段。
說白了,外觀模式就是一個函數,封裝了復雜的操作。
代碼實現:
比如一個跨瀏覽器的ajax調用
function ajaxCall(type,url,callback,data){ // 根據當前瀏覽器獲取對ajax連接對象的引用
var xhr=(function(){ try { // 所有現代瀏覽器所使用的標準方法
return new XMLHttpRequest();
}catch(e){}
// 較老版本的internet Explorer兼容
try{ return new ActiveXObject("Msxml2.XMLHTTP.6.0");
}catch(e){} try{ return new ActiveXObject("Msxml2.XMLHTTP.3.0");
}catch(e){} try{ return new ActiveXObject("Microsoft.XMLHTTP");
}catch(e){} // 如果沒能找到相關的ajax連接對象,則跑出一個錯誤。
throw new Error("Ajax not support in this browser.")
}()),
STATE_LOADED=4,
STATUS_OK=200; // 一但從服務器收到表示成功的相應消息,則執行所給定的回調方法
xhr.onreadystatechange=function{ if(xhr.readyState !==STATE_LOADED){ return;
} if(xhr.state==STATUS_OK){
callback(xhr.responseText);
}
} // 使用瀏覽器的ajax連接對象來向所給定的URL發出相關的調用 xhr.open(type.toUpperCase(),url);
xhr.send(data);
}// 使用方法ajaxCall("get","/user/12345",function(rs){
alert('收到的數據為:'+rs);
})
應用場景:
當需要通過一個單獨的函數或方法來訪問一系列的函數或方法調用,以簡化代碼庫的其余內容,使得代碼更容易跟蹤管理或者更好的維護時,可以使用外觀模式。其實我們平時代碼中這種模式應該是用的比較多的。
javascript的設計模式有很多種,本文只是總結了其中的幾種,以后可能會補充。這篇文章下來查閱了挺多資料,也學到挺多東西的。
由于能力有限,有誤之處,歡迎指出
*請認真填寫需求信息,我們會在24小時內與您取得聯系。