外观模式(Facade Pattern)是一种结构型设计模式,其主要目的是简化复杂系统的接口并提供一个更高级别的接口以供外部使用。
可以将外观模式想象成一个门面或者外观,类似于房子的门面,它把整个系统隐藏在其背后。对于外部使用者而言,只需要通过门面提供的接口来操作系统,而不需要关心背后的实现细节。
外观模式的一个生动的例子是手机的操作界面。手机的操作界面为用户提供了一个简单易用的接口,可以通过点击屏幕上的图标、按钮来进行操作,但实际上在背后有许多不同的系统组件在协作工作。用户不需要关心这些组件的具体实现,只需要使用操作界面提供的接口即可。
很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。
(1)兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
(2)封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
优点
1.减少系统相互依赖。外观模式可以将客户端和子系统解耦,因为客户端只需要与外观对象交互,而不需要了解底层子系统的实现细节。
2.提高灵活性。外观模式可以提高系统的灵活性,因为它提供了一个更简单的接口,使得客户端可以更容易地使用系统中的功能。
3. 提高了安全性。因为外观对象可以控制客户端对子系统的访问,并提供一个安全的接口。
缺点
1.不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
例如,如果系统需要增加一个新的子系统,那么需要在外观类中增加一个对应的方法,这样就会修改外观类的代码,从而可能导致客户端的代码也需要修改。
代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。代理模式的生动理解可以类比为一个人找代理去购物。这个人想购买一些物品,但是由于某些原因无法亲自前往商店购买,于是他找到了一个代理人去帮他购买。
代理模式中的代理对象就像是这个代理人,它可以代替真实对象进行一些操作。
例子1:
假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:
let Flower = function () {}
let xiaoming = {
sendFlower: function (target) {
let flower = new Flower();
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function (flower) {
A.listenGoodMood(function () {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function (flower) {
console.log('收到花', flower)
},
listenGoodMood: function (fn) {
setTimeout(function () {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B);
例子2:
HTML元 素事件代理
- 1
- 2
- 3
例子3:
假设有一个图片加载的场景,可以使用代理模式来实现图片的懒加载。具体地,当用户滚动页面时,只加载可视区域内的图片,而不加载整个页面的所有图片。这样可以减少网络请求,提高页面加载速度。
// js部分
function lazyLoadImage(image) {
// 模拟图片加载
image.src = image.dataset.src;
image.removeAttribute('data-src');
}
const imageContainer = document.querySelector('#container');
const imageList = imageContainer.querySelectorAll('img');
// 使用代理模式
const lazyLoadImageProxy = new Proxy(lazyLoadImage, {
apply: function (target, thisArg, args) {
const image = args[0];
const rect = image.getBoundingClientRect();
// 只加载可视区域内的图片
if (rect.top < window.innerHeight) {
return target.apply(thisArg, args);
}
}
});
for (let i = 0; i < imageList.length; i++) {
imageList[i].addEventListener('load', function () {
// 确保图片加载后再添加代理
this.removeEventListener('load', arguments.callee);
const imageProxy = new Proxy(lazyLoadImageProxy, {
get: function (target, property) {
if (property === 'src') {
// 先显示占位图
this.setAttribute('src', 'placeholder.jpg');
// 再使用代理加载图片
return target;
} else {
return target[property];
}
}
});
imageProxy(this);
});
}
在这个场景中,代理对象可以负责判断图片是否在可视区域内,如果在可视区域内,就调用实际对象加载图片;如果不在可视区域内,就不加载图片。同时,代理对象还可以在图片加载完成后,缓存图片的信息,以便下次使用。这样可以提高用户的体验,同时也减轻了服务器的负担。
优点
缺点
工厂模式(Frontend Factory Pattern)是一种创建对象的设计模式,它可以用于解决对象创建时的复杂度和重复性问题。通过工厂模式,我们可以将对象的创建封装到一个工厂函数中,从而简化对象创建的过程。
假设有一个汽车生产工厂,工厂里有多条生产线,每条生产线都有自己的特定功能,比如制造车身、组装发动机、安装座椅等。当有客户需要购买汽车时,工厂会根据客户的要求,在不同的生产线上生产出不同的汽车,最终交给客户。
通常来说,工厂模式包含以下几个角色:
工厂函数(Factory):一个用于创建其他对象的函数,它负责创建对象并返回。
抽象接口(Interface):定义了工厂函数创建对象所需的基本结构和属性。
具体实现(Concrete):实现抽象接口定义的基本结构和属性,同时也可以包含其他的自定义属性和方法。
// 定义抽象接口
class Button {
constructor(text) {
this.text = text;
}
render() {
console.log(`Rendering ${this.text} button...`);
}
}
// 定义具体实现
class SubmitButton extends Button {
constructor() {
super('Submit');
this.disabled = false;
}
render() {
console.log(`Rendering ${this.text} button...`);
console.log(`Disabled: ${this.disabled}`);
}
}
// 定义工厂函数
function createButton(type) {
switch(type) {
case 'submit':
return new SubmitButton();
default:
return new Button('Default');
}
}
// 创建按钮实例
const myButton = createButton('submit');
myButton.render(); // 输出 "Rendering Submit button... Disabled: false"
场景
优点
缺点
单例模式是一种创建型设计模式
顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。
想象一下,你正在编写一个游戏,需要一个全局的游戏管理器,它负责处理所有游戏相关的事务,例如:渲染游戏画面、处理用户输入、处理游戏逻辑等等。为了确保只有一个游戏管理器实例,并且在全局范围内能够访问它,你可以使用单例模式来实现这一点
class GameManager {
constructor() {
if (GameManager.instance) {
return GameManager.instance;
}
this.score = 0;
GameManager.instance = this;
}
static getInstance() {
return GameManager.instance || new GameManager();
}
addScore(points) {
this.score += points;
console.log(`Score: ${this.score}`);
}
}
// 使用单例
const gameManager1 = new GameManager();
const gameManager2 = GameManager.getInstance();
gameManager1.addScore(10); // Score: 10
gameManager2.addScore(20); // Score: 30
console.log(gameManager1 === gameManager2); // true
我们一般通过实现以下两点来解决上述问题:
getInstance()
方法来创建/获取唯一实例。Javascript中单例模式可以通过以下方式实现:
// 单例构造器
const FooServiceSingleton = (function () {
// 隐藏的Class的构造函数
function FooService() {}
// 未初始化的单例对象
let fooService;
return {
// 创建/获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
实现的关键点有:
我们可以验证下单例对象是否创建成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
场景例子
var myNamespace = myNamespace || {};
myNamespace.Singleton = (function() {
// 私有属性和方法
var privateProperty = 'Hello World';
function privateMethod() {
console.log(privateProperty);
}
// 创建单例对象
var instance;
function init() {
// 公共方法
function publicMethod() {
console.log('The public can see me!');
privateMethod();
}
// 公共属性
var publicProperty = 'I am also public';
// 返回公共接口
return {
publicMethod: publicMethod,
publicProperty: publicProperty
};
}
// 返回单例对象
return {
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
// 使用单例对象
var singleA = myNamespace.Singleton.getInstance();
var singleB = myNamespace.Singleton.getInstance();
console.log(singleA === singleB); // true
singleA.publicMethod(); // The public can see me! Hello World
var LoginModal = (function() {
var instance;
function init() {
// 创建登录框 DOM 元素
var loginModal = document.createElement('div');
loginModal.innerHTML = 'Login
';
document.body.appendChild(loginModal);
// 绑定登录表单的提交事件
var loginForm = loginModal.querySelector('form');
loginForm.addEventListener('submit', function(event) {
event.preventDefault();
var username = loginForm.querySelector('input[type="text"]').value;
var password = loginForm.querySelector('input[type="password"]').value;
console.log('username: ' + username + ', password: ' + password);
// TODO: 发送登录请求
});
// 返回登录框 DOM 元素
return loginModal;
}
return {
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
// 使用登录框单例对象
var loginModalA = LoginModal.getInstance();
var loginModalB = LoginModal.getInstance();
console.log(loginModalA === loginModalB); // true
优点
缺点
策略模式是一种行为型设计模式
策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换。
可以想象一下选择支付方式的场景。假设有三种支付方式:支付宝、微信和银行卡。在支付过程中,用户需要选择一种支付方式,然后根据所选的支付方式来进行支付操作。在这个场景中,支付方式就是算法,而选择支付方式的过程就是策略。
策略模式-校验表单
场景例子
优点
缺点
迭代器模式是一种行为型设计模式,用于提供一种顺序访问聚合对象元素的方法,而不需要了解聚合对象的内部结构。这种模式在许多编程语言中被广泛使用,例如 JavaScript 中的迭代器。
以 JavaScript 中的数组为例,我们可以使用 for 循环遍历数组的每个元素,如:
const arr = [1, 2, 3, 4, 5];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
上述代码中的 for 循环就是一种迭代器的实现方式。它提供了一个统一的接口来访问数组中的每个元素,并隐藏了数组内部的实现细节。另外,我们还可以使用其他迭代器实现方式,例如 forEach
,map
,filter
等等。这些迭代器都提供了一种简洁易懂、易于维护的访问方式。
迭代器模式的优点在于它能够将遍历与聚合分离,使得聚合对象的实现更加简单和灵活,同时也使得迭代算法更加易于重用和修改。
迭代器模式解决了以下问题:
一个迭代器通常需要实现以下接口:
为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
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会自动得到通知并更新自己的状态。
一个生动的例子是电视台与观众之间的关系。电视台是被观察者,观众是观察者。当电视台播出新节目时,所有观众都会收到通知,并根据自己的兴趣决定是否收看该节目。同样的,当电视台取消或改变节目时,所有观众也会得到通知并相应地做出调整。
观察者模式中Subject(被观察对象)对象一般需要实现以下API:
用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!
场景
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
优点
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
场景
例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。
var goods = { //手机库存
'red|32G': 3,
'red|64G': 1,
'blue|32G': 7,
'blue|32G': 6,
};
//中介者
var mediator = (function() {
var colorSelect = document.getElementById('colorSelect');
var memorySelect = document.getElementById('memorySelect');
var numSelect = document.getElementById('numSelect');
return {
changed: function(obj) {
switch(obj){
case colorSelect:
//TODO 更新商品颜色信息
break;
case memorySelect:
//TODO 更新商品内存信息
break;
case numSelect:
//TODO 更新商品数量信息
break;
}
}
}
}
})();
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numSelect.onchange = function() {
mediator.changed(this);
};
聊天室成员类:
function Member(name) {
this.name = name;
this.chatroom = null;
}
Member.prototype = {
// 发送消息
send: function (message, toMember) {
this.chatroom.send(message, this, toMember);
},
// 接收消息
receive: function (message, fromMember) {
console.log(`${fromMember.name} to ${this.name}: ${message}`);
}
}
聊天室类:
function Chatroom() {
this.members = {};
}
Chatroom.prototype = {
// 增加成员
addMember: function (member) {
this.members[member.name] = member;
member.chatroom = this;
},
// 发送消息
send: function (message, fromMember, toMember) {
toMember.receive(message, fromMember);
}
}
测试一下:
const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');
chatroom.addMember(bruce);
chatroom.addMember(frank);
bruce.send('Hey frank', frank);
//输出:bruce to frank: hello frank
优点
缺点
系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。
访问者模式(Visitor Pattern)是一种行为型设计模式,它的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
假设我们正在开发一个旅游网站,我们的网站有许多旅游景点的介绍页面。我们想要为我们的用户提供一种收集信息的功能,可以让用户查看他们访问过的所有景点的总票价。
我们可以通过访问者模式来实现这一功能。我们可以将每个景点都表示为一个对象,每个景点对象都有一个
accept(visitor)
方法,允许访问者访问景点,并计算票价。我们可以定义一个名为Visitor
的接口,然后实现一个TicketPriceVisitor
类,它可以对每个景点进行价格计算并返回票价。
// 定义 Visitor 接口
class Visitor {
visit(element) {
throw new Error('visit method must be implemented');
}
}
// 景点类
class SightseeingLocation {
constructor(name, ticketPrice) {
this.name = name;
this.ticketPrice = ticketPrice;
}
accept(visitor) {
return visitor.visit(this);
}
}
// 票价访问者类
class TicketPriceVisitor extends Visitor {
constructor() {
super();
this.totalPrice = 0;
}
visit(location) {
this.totalPrice += location.ticketPrice;
}
}
// 创建景点对象
const beijing = new SightseeingLocation('北京', 100);
const shanghai = new SightseeingLocation('上海', 80);
const guilin = new SightseeingLocation('桂林', 120);
const hangzhou = new SightseeingLocation('杭州', 90);
// 计算总票价
const visitor = new TicketPriceVisitor();
beijing.accept(visitor);
shanghai.accept(visitor);
guilin.accept(visitor);
hangzhou.accept(visitor);
console.log(`总票价:${visitor.totalPrice} 元`);
// 访问者
class Visitor {
constructor() {}
visitConcreteElement(ConcreteElement) {
ConcreteElement.operation()
}
}
// 元素类
class ConcreteElement{
constructor() {
}
operation() {
console.log("ConcreteElement.operation invoked");
}
accept(visitor) {
visitor.visitConcreteElement(this)
}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
访问者模式的实现有以下几个要素:
Receiving Object:
function Employee(name, salary) {
this.name = name;
this.salary = salary;
}
Employee.prototype = {
getSalary: function () {
return this.salary;
},
setSalary: function (salary) {
this.salary = salary;
},
accept: function (visitor) {
visitor.visit(this);
}
}
Visitor Object:
function Visitor() { }
Visitor.prototype = {
visit: function (employee) {
employee.setSalary(employee.getSalary() * 2);
}
}
验证一下:
const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);
console.log(employee.getSalary());//输出:2000
场景
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
优点
缺点