javascript设计模式与应用
目录
前言
设计模式真的很多很复杂,建议有兴趣的看我文章最后的链接去学习,要想掌握和熟练应用到项目中绝对不是一蹴而就的,我这篇文章顶多就是一个入门级别的学习,让大家对设计模式有个概念,我讲的也非常简单,代码太长我自己都懒得看,所以尽量举简单的例子,说实话,设计模式我啃得也很痛苦,说多了都是泪,哈哈!最后说明一下,文章并未列出23种设计模式,我只按照我觉得重要和使用多的讲了上面11个,也并非所有的模式都写了实现代码,后续有时间和必要的话,可能还会更新其他的模式。
设计模式
构造函数模式
用构造函数来生成对象
// 实例共享的方法定义在原型上,实例本身的属性定义在构造函数里面
function Car( model, year, miles ) {
this.model = model;
this.year = year;
this.miles = miles;
}
// 这里注意要在原型上添加方法,而不是给原型赋值,不然就会丢失原型
Car.prototype.toString = function () {
return this.model + " has done " + this.miles + " miles";
};
// Usage:
var civic = new Car( "Honda Civic", 2009, 20000 );
var mondeo = new Car( "Ford Mondeo", 2010, 5000 );
console.log( civic.toString() );
console.log( mondeo.toString() );
工厂模式(Factory)
- 定义: 工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。除非项目很复杂,否则一般用不到工厂方法和抽象方法
- 应用:jQuery的$选择器就是用工厂模式创建的
// 简单模拟一下jQuery的实现
class jQuery {
constructor(selector) {
let slice = Array.prototype.slice
let dom = slice.call(document.querySelectorAll(selector))
let len = dom.length? dom.length: 0
for (let i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector
}
addClass() {
}
...
}
// $就是一个工厂
window.$ = function(selector) {
return new jQuery(selector)
}
单例模式(Singleton)
- 系统中只能有一个实例,一个类只能创建一个实例, 比如登陆框,购物车, vuex和redux的store也是单例
- 来实现一个单例
class Singleton {
login() {
}
}
Singleton.getInstance = (function() {
let instance
return function() {
if(!instance) {
instance = new Singleton()
}
return instance
}
})()
let obj1 = Singleton.getInstance()
let obj2 = Singleton.getInstance()
console.log(obj1 === obj2) // true
代理模式(Proxy)
- 使用者无权访问目标对象, 为其他对象提供一种代理以控制对这个对象的访问, 接口不变
- 网页事件代理,jQuery.$.proxy, es6的Proxy
- es6的代理proxy
- 在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式, 接下来我们自己用Proxy实现一下吧
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value, property);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 };
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`);
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`);
}
);
p.a = 2; // 监听到属性a改变
p.a; // 'a' = 2
观察者模式(Observer)
- 主题和观察者分离,当主题更新的时候,通知所有的观察者更新自己
- 应用:vue的响应式,node的eventEmitter, 我们来实现一个简单的EventEmitter
- 观察者模式和发布订阅者模式还是有点区别的,在这里不做区分了
class EventEmitter {
constructor() {
this._events = {}; // 维护订阅者列表
}
// 订阅主题
on(name, fn) {
if (name in this._events) {
// 避免重复订阅
if(!this._events[name].find(f => f === fn)) {
this_events[name].push(fn);
}
} else {
this._events[name] = [];
this._events[name].push(fn);
}
}
// 发布主题,相关主题的订阅者更新
emit(name, ...arg) {
if (name in this._events) {
let events = this._events[name];
for (let i = 0; i < events.length; i++) {
events[i](...arg);
}
}
}
// 取消订阅
off(name, fn) {
if (name in this._events) {
let index = this._events[name].findIndex(f => f === fn);
if (index > -1) {
this._events[name].splice(index, 1);
}
}
}
}
let event = new EventEmitter();
function subFn(data) {
console.log(data);
}
// 订阅主题
event.on("vue", subFn);
// 发布通知
event.emit("vue", "vue3.0要出来了"); // "vue3.0要出来了"
// 取消订阅
event.off("vue", subFn);
// 再发布通知,就不会打印了
event.emit("vue", "vue3.0马上要出来了");
适配器模式(Adaptor)
- 适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。
- 旧接口和用户分离
装饰器模式(Decorator)
- 为对象添加新功能,不改变原有的结构和功能, 优点是把类(函数)的核心职责和装饰功能区分开了
- ES7已经有了装饰器
阮一峰的ES6教程中对装饰器讲的很好很全面,大家可以去看看,链接在此(es6.ruanyifeng.com/#docs/decor…)
迭代器模式(Iterator)
- 提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。
- 迭代器的几个特点是:
- 访问一个聚合对象的内容而无需暴露它的内部表示。
- 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
- 遍历的同时更改迭代器所在的集合结构可能会导致问题(比如C#的foreach里不允许修改item。
- 应用
- es6的Iterator es6的有序数据集合(Array, string, Map, Set, generator等)都部署了[Symbol.iterator]属性, 这个属性是一个方法,返回一个迭代器,因此都可以用for...of遍历
- jQuery里一个非常有名的迭代器就是$.each方法,通过each我们可以传入额外的function,然后来对所有的item项进行迭代操作,例如:
$.each(['dudu', 'dudu', '酸奶小妹', '那个MM'], function (index, value) {
console.log(index + ': ' + value);
});
//或者
$('li').each(function (index) {
console.log(index + ': ' + $(this).text());
});
外观模式(Facade)
- 定义
- 为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 特点
-
外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦。外观模式经常被认为开发者必备,它可以将一些复杂操作封装起来,并创建一个简单的接口用于调用。
-
外观模式经常被用于JavaScript类库里,通过它封装一些接口用于兼容多浏览器,外观模式可以让我们间接调用子系统,从而避免因直接访问子系统而产生不必要的错误。
-
外观模式的优势是易于使用,而且本身也比较轻量级。但也有缺点 外观模式被开发者连续使用时会产生一定的性能问题,因为在每次调用时都要检测功能的可用性
-
- js中ie浏览器的事件api和其他浏览器的不同,为了兼容,我们一般都会封装一个统一的事件处理函数
var addMyEvent = function (el, event, fn) {
if (el.addEventListener) {
el.addEventListener(event, fn, false);
} else if (el.attachEvent) {
el.attachEvent('on' + event, fn);
} else {
el['on' + event] = fn;
}
};
状态模式(State)
- 一个对象有状态变化,每个状态下有不同的行为
- 场景:
- 有限状态机
- es6的Promise
- 实现一个Promise, 甩链接,有兴趣的同学可以去研究
大白话promise
谈谈 ES6 的 Promise 对象
手把手教你实现一个完整的 Promise
promise源码分析
【翻译】Promises/A+规范
命令模式(Command)
- 命令模式(Command)的定义是:用于将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及执行可撤销的操作。也就是说改模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理。此外,可以通过调用实现具体函数的对象来解耦命令对象与接收对象。
我们现在有一个汽车管理类, 如下
(function() {
var carManager = {
// 获取汽车的信息
requestInfo: function(model, id) {
return "The information for " + model + " with ID " + id + " is foobar";
},
// 购买汽车
buyVehicle: function(model, id) {
return "You have successfully purchased Item " + id + ", a " + model;
},
// 组织车展
arrangeViewing: function(model, id) {
return (
"You have successfully booked a viewing of " +
model +
" ( " +
id +
" ) "
);
}
}
})();
// 调用方法
carManager.requestInfo( "Ferrari", "14523" );
carManager.buyVehicle( "Ford Mondeo", "54323" );
carManager.arrangeViewing("Ford Escort", "34232" );
然而在一些情况下,我们并不想直接调用对象内部的方法。这样会增加对象与对象间的依赖。现在我们来扩展一下这个CarManager, 使其能够接受任何来自包括model和car ID 的CarManager对象的处理请求。根据命令模式的定义,我们希望实现如下这种功能的调用:
CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });
根据这样的需求,我们可以这样实现CarManager.execute方法:
CarManager.execute = function (command) {
return CarManager[command.request](command.model, command.carID);
};
改造以后,调用就简单多了,如下调用都可以实现
CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });
参考资料: