文地址: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小时内与您取得联系。