转载自: mp.weixin.qq.com/s/uMwOIqjHZ…
创建型设计模式
- 单例模式
- 工厂模式
- 抽象工厂模式
- 建造者模式
- 原型模式
结构型设计模式
- 桥接模式
- 代理模式
- 装饰器模式
- 适配器模式
- 享元模式
- 组合模式
- 门面(外观)模式
行为型设计模式
- 观察者模式
- 模板模式
- 策略模式
- 职责链模式
- 状态模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 命令模式
- 解释器模式
- 中介模式
创建型设计模式
- 单例模式
- 工厂模式
- 抽象工厂模式
- 建造者模式
- 原型模式
单例模式
什么是单例模式?
单例模式的定义是 保证一个类仅有一个实例,并提供一个访问它的全局访问点。简单说就是只有一个对象。用一个变量来标识当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
es5 单例模式
- 可以在构造函数定义静态方法,此方法进行对对象 创造唯一性的判断,如果没有就new 构造函数,如果有直接return ,但是这种方法调用时候还要用 构造函数的静态方法形式,这种调用形式,让不知道不了解,很懵逼,因为常规都是new 来调用。
- 既然不透明,来个透明方法,整体包在 a构造函数中,这个a是一个匿名函数加闭包,也就是a函数是个自执行函数然后里面return出来一个同名的a函数,构成闭包,里面a函数 包含 判断是否已经创建了a对象,以及执行一个业务逻辑,将this给instance相当于instance就是这个函数,return出来 就是new后面的那个函数。
方案一:
var singleton = function(name){
this.name = name;
this.instance = null
}
singleton.prototype.getName = function(){
alert(this.name)// 原型上的方法 this指向的是实例,因为实例调用的这个方法,this.name指向的是seven1
}
singleton.getInstance = function(name){
if(!this.instance){
this.instance = new singleton(name);
}
console.log(this,this.instance)// 静态方法的this指向的构造函数本身,this.instance指的就是new singleton
return this.instance
}
var a = singleton.getInstance('seven1')
方案二:
var createDiv = (function(){
var instance;
var CreateDiv = function(html){
if(instance){
return instance;
}
this.html = html;
this.init();
return instance = this;
}
CreateDiv.prototype.init = function(){
var div = document.createElement('div');
div.innerHtml = this.html;
document.body.appendChild(div);
}
return CreateDiv
})()
var a = new CreateDiv('seven1');
这样分离还不够细,因为业务的逻辑是混杂init里面的,如果改逻辑,这个函数就要改,所以我们理想应该是,业务逻辑一个,控制业务逻辑的函数一个,然后再一个单例模式控制唯一性的函数一个。
代理模式下单例
var CreateDiv = function(html) {
this.html = html;
this.init();
};
CreateDiv.prototype.init = function() {
var div = document.createElement('div');
div.innerHTML = this.html;
document.body.appendChild(div);
}
var ProxySingletonCreateDiv = (function() {
var instance;
return function(html) {
if (!instance) {
instance = new CreateDiv(html);
}
return instance;
}
})();
var a = new ProxySingletonCreateDiv('seven1');
var b = new ProxySingletonCreateDiv('seven2');
惰性模式下单例
上面的timeTool实际上是一个函数,_instance作为实例对象最开始赋值为null,init函数是其构造函数,用于实例化对象,立即执行函数返回的是匿名函数用于判断实例是否创建,只有当调用timeTool()时进行实例的实例化,这就是惰性单例的应用,不在js加载时就进行实例化创建, 而是在需要的时候再进行单例的创建。如果再次调用, 那么返回的永远是第一次实例化后的实例对象。
let timeTool = (function() {
let _instance = null;
function init() {
//私有变量
let now = new Date();
//公用属性和方法
this.name = '处理时间工具库',
this.getISODate = function() {
return now.toISOString();
}
this.getUTCDate = function() {
return now.toUTCString();
}
}
return function() {
if(!_instance) {
_instance = new init();
}
return _instance;
}
})()
单例模式的应用
- 全局作用域下的变量
例如: var a= {}, 独一无二,并且全局作用域,但是问题很多,命名空间污染,易覆盖。
解决办法: 使用命名空间 es5
var nameSpace={
a: function(){},
b: function(){}
}
es6
let timeTool = {
name: '处理时间工具库',
getISODate: function() {},
getUTCDate: function() {}
}
全局只暴露了一个timeTool对象, 在需要使用时, 只需要采用timeTool.getISODate()调用即可。timeTool对象就是单例模式的体现。
在JavaScript创建对象的方式十分灵活, 可以直接通过对象字面量的方式实例化一个对象, 而其他面向对象的语言必须使用类进行实例化。所以,这里的timeTool就已经是一个实例, 且ES6中let和const不允许重复声明的特性,确保了timeTool不能被重新覆盖。
- 管理模块
var devA = (function(){
//ajax模块
var ajax = {
get: function(api, obj) {console.log('ajax get调用')},
post: function(api, obj) {}
}
//dom模块
var dom = {
get: function() {},
create: function() {}
}
//event模块
var event = {
add: function() {},
remove: function() {}
}
return {
ajax: ajax,
dom: dom,
event: event
}
})()
库中有ajax,dom和event三个模块,用同一个命名空间devA来管理。在进行相应操作的时候.只需要devA.ajax.get()进行调用即可。这样可以让库的功能更加清晰.
- vuex
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
// 安装vuex插件
Vue.use(Vuex)
// 将store注入到Vue实例中
new Vue({
el: '#app',
store
})
let Vue // Vue的作用和上面的instance一样
...
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
通过调用Vue.use()方法,安装 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue实例里去。
上面便是 Vuex 源码中单例模式的实现办法了,套路可以说和getInstance如出一辙。通过这种方式,可以保证一个 Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。
- es6中应用单例模式
ES6中提供了为class提供了static关键字定义静态方法, 我们可以将constructor中判断是否实例化的逻辑放入一个静态方法getInstance中,调用该静态方法获取实例,constructor中只包需含实例化所需的代码,这样能增强代码的可读性、结构更加优化。
class SingletonApple {
constructor(name, creator, products) {
//首次使用构造器实例
if (!SingletonApple.instance) {
this.name = name;
this.creator = creator;
this.products = products;
//将this挂载到SingletonApple这个类的instance属性上
SingletonApple.instance = this;
}
return SingletonApple.instance;
}
}
let appleCompany = new SingletonApple('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod']);
console.log(appleCompany === copyApple); //true
静态方法优化版
class SingletonApple {
constructor(name, creator, products) {
this.name = name;
this.creator = creator;
this.products = products;
}
//静态方法
static getInstance(name, creator, products) {
if(!this.instance) {
this.instance = new SingletonApple(name, creator, products);
}
return this.instance;
}
}
let appleCompany = SingletonApple.getInstance('苹果公司', '乔布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = SingletonApple.getInstance('苹果公司', '阿辉', ['iPhone', 'iMac', 'iPad', 'iPod'])
console.log(appleCompany === copyApple); //true
单例模式虽然简单,但是在项目中的应用场景却是相当多的,单例模式的核心是确保只有一个实例, 并提供全局访问。就像我们只需要一个浏览器的window对象, jQuery的$对象而不再需要第二个。由于JavaScript代码书写方式十分灵活, 这也导致了如果没有严格的规范的情况下,大型的项目中JavaScript不利于多人协同开发, 使用单例模式进行命名空间,管理模块是一个很好的开发习惯,能够有效的解决协同开发变量冲突的问题。灵活使用单例模式,也能够减少不必要的内存开销,提高用于体验。
工厂模式
应用场景
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
解决方案
enum HelloType {
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
适用场景
- 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
- 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
- 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
优点
- 创建对象的过程可能很复杂,但我们只需要关心创建结果。
- 构造函数和创建者分离, 符合“开闭原则”
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
- 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
- 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
什么时候不用
当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。
由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。
例子
曾经我们熟悉的JQuery的$()就是一个工厂函数,它根据传入参数的不同创建元素或者去寻找上下文中的元素,创建成相应的jQuery对象
class jQuery {
constructor(selector) {
super(selector)
}
add() {
}
// 此处省略若干API
}
window.$ = function(selector) {
return new jQuery(selector)
}
vue 的异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
抽象工厂模式
应用场景
继承同一父类、实现同一接口的子类对象,由给定的多个类型参数创建具体的对象。
解决方案
enum Type {
A,
B
}
enum Occupation {
TEACHER,
STUDENT
}
interface Hello {
sayHello()
}
class TA implements Hello {
sayHello() {
console.log('Teacher A say hello')
}
}
class TB implements Hello {
sayHello() {
console.log('Teacher B say hello')
}
}
class SA implements Hello {
sayHello() {
console.log('Student A say hello')
}
}
class SB implements Hello {
sayHello() {
console.log('Student B say hello')
}
}
class AFactory {
static list = new Map<Occupation, Hello>([
[Occupation.TEACHER, new TA()],
[Occupation.STUDENT, new SA()]
])
static getHello(occupation: Occupation) {
return AFactory.list.get(occupation)
}
}
class BFactory {
static list = new Map<Occupation, Hello>([
[Occupation.TEACHER, new TB()],
[Occupation.STUDENT, new SB()]
])
static getHello(occupation: Occupation) {
return BFactory.list.get(occupation)
}
}
class HelloFactory {
static list = new Map<Type, AFactory | BFactory>([
[Type.A, AFactory],
[Type.B, BFactory]
])
static getType(type: Type) {
return HelloFactory.list.get(type)
}
}
// test
HelloFactory.getType(Type.A).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.A).getHello(Occupation.STUDENT).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.STUDENT).sayHello()
建造者模式
建造者模式将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。工厂模式主要是为了创建对象实例或者类簇(抽象工厂),关心的是最终产出(创建)的是什么,而不关心创建的过程。而建造者模式关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节。
应用场景
- 创建时有很多必填参数需要验证。
- 创建时参数求值有先后顺序、相互依赖。
- 创建有很多步骤,全部成功才能创建对象。
解决方案
class Programmer {
age: number
username: string
color: string
area: string
constructor(p) {
this.age = p.age
this.username = p.username
this.color = p.color
this.area = p.area
}
toString() {
console.log(this)
}
}
class Builder {
age: number
username: string
color: string
area: string
build() {
if (this.age && this.username && this.color && this.area) {
return new Programmer(this)
} else {
throw new Error('缺少信息')
}
}
setAge(age: number) {
if (age > 18 && age < 36) {
this.age = age
return this
} else {
throw new Error('年龄不合适')
}
}
setUsername(username: string) {
if (username !== '小明') {
this.username = username
return this
} else {
throw new Error('小明不合适')
}
}
setColor(color: string) {
if (color !== 'yellow') {
this.color = color
return this
} else {
throw new Error('yellow不合适')
}
}
setArea(area: string) {
this.area = area
return this
}
}
// test
const p = new Builder()
.setAge(20)
.setUsername('小红')
.setColor('red')
.setArea('hz')
.build()
.toString()
原型模式
原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Student extends Person {
constructor(name) {
super(name)
}
sayHello() {
console.log(`Hello, My name is ${this.name}`)
}
}
let student = new Student("xiaoming")
student.sayHello()
应用场景
- 原型模式是基于已有的对象克隆数据,而不是修改原型链!
- 创建对象的代价太大,而同类的不同实例对象属性值基本一致。通过原型克隆的方式节约资源。
- 不可变对象通过浅克隆实现。
- 可变对象通过深克隆实现,深克隆占用资源多。
- 同一对象不同时间版本,可以对比没变化的浅克隆,变化的深克隆,然后新版本替换旧版本。
结构型设计模式
- 桥接模式
- 代理模式
- 装饰器模式
- 适配器模式
- 享元模式
- 组合模式
- 门面(外观)模式
桥接模式
桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。
应用场景
- 将抽象和实现解耦,让它们可以独立变化。
- 一个类存在多个独立变化的维度,我们通过组合的方式,让多个维度可以独立进行扩展。
- 非常类似于组合优于继承原则。
解决方案
class Color {
constructor(name){
this.name = name
}
}
class Shape {
constructor(name,color){
this.name = name
this.color = color
}
draw(){
console.log(`${this.color.name} ${this.name}`)
}
}
//测试
let red = new Color('red')
let yellow = new Color('yellow')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()
优点
- 有助于独立地管理各组成部分, 把抽象化与实现化解耦
- 提高可扩充性
缺点
- 大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。
代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。我们实际访问的是替身对象,替身对象对请求做出一些处理之后,再把请求转交给本体对象。
什么是保护代理以及虚拟代理?
代理可以为对象过滤一些请求,这种请求就可以直接在代理处过滤掉,这种叫保护代理,代理会在对象合适的时机再执行要代理执行的操作,这叫做虚拟代理,虚拟代理会把一些开销很大的对象,延迟代需要它的时候再去创建。
var b={
receiveFlower: function(flower){
A.listenGoodMood(function(){
var flower = new Flower();
A.receiveFlower(flower);
})
}
}
保护代理用于控制不同权限对象对目标对象的访问,js 不容易实现保护代理,我们无法判断谁访问了这个对象,虚拟代理用的比较多。
例子:虚拟代理实现图片预加载
/*
* 无代理,更常见的情况
*/
var MyImage = (function () {
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
var img = new Image
img.onload = function () {
imgNode.src = img.src
}
return {
setSrc: function (src) {
imgNode.src = '***.gif'
img.src = src
}
}
})
/*
* 引入代码
*/
var myImage = (function () {
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return {
setSrc: function (src) {
imgNode.src = src
}
}
})()
var proxyImage = (function () {
var img = new Image
img.onload = function () {
myImage.setSrc(this.src)
}
return {
setSrc: function (src) {
myImage.setSrc('***.gif')
img.src = src
}
}
})()
proxyImage.setSrc('***.JPG')
虚拟代理http请求
/*
* 点击 checkbox 即同步文件
* 反面例子
*/
var syncFile = function (id) {
console.log('开始同步文件,id 为' + id)
}
var checkbox = document.getElementsByTagName('input')
for (var i = 0, c; c = checkbox[i++]; ) {
c.onclick = function () {
if (this.checked === true) {
syncFile(this.id)
}
}
}
// 使用代理
var syncFile = function (id) {
console.log('开始同步文件,id 为' + id)
}
var proxySyncFile = (function () {
var cache = []
var timer
return function (id) {
cache.push(id)
if (timer) {
return
}
timer = setTimeout(function () {
syncFile(cache.join(',')) // 2 秒后向本体发送需要同步的 ID 集合。
clearTimeout(timer)
timer = null
cache.length = 0
}, 2000)
}
})()
var checkbox = document.getElementsByTagName('input')
for (var i = 0, c; c = checkbox[i++]; ) {
c.onclick = function () {
if (this.checked === true) {
proxySyncFile(this.id)
}
}
}
ES6 proxy
proxy理解为在目标对象target之前假设一个handler处理函数,外界对目标对象的访问必须先经过handle处理,在语言层面进行的改变,属于一种元编程,可以重新定义对象的一些方法以及get set行为,实际上重载了点运算符,即用自己的定义覆盖柯语言的原始定义。
表达式:var proxy = new Proxy(target, handler);
target目标对象,handle也是一个对象,用来定制拦截行为。
get函数应用:
- 用在获取对象属性时候,加上proxy后会将之前错误处理返回underfind改为 throw error,get 方法可以继承。
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property "" + property + "" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
-----------------
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
- 可以实现类似函数编程的pipe 管道 函数连接效果,将get,获取对象属性的操作改为 转变执行函数的操作,从而实现属性的链式操作。
var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
- get拦截,实现数组读取负数的索引
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
vue3的代理模式
vue3 采用了ES2015的Proxy来代替Object.defineProperty可以做到监听对象属性的增删和数组元素和长度的修改,优化了2.0对应数组监监听不到对象属性的增删、数组元素和长度的变化的这一影响使用体验的地方,还可以监听Map、Set、WeakSet、WeakMap。而且做到了按需监听,监听动作均在使用时产生。
装饰者模式
什么是装饰者模式?是为了解决什么问题?
装饰者模式是给对象动态增加职责的方式,装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态添加职责。
解决问题:在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活, 还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之 改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。
传统面向对象的装饰器模式与普通js添加方法区别?
基本要点就是:并没有真正改变对象自身,而是将对象放入另一个对象中,这些对象以链式引用,a 放入b中,b放入c中,所以 c就拥有了 a和b的方法。普通的js改变了对象自身。
装饰者模式将一个对象嵌入另一个对象中,实际上相当于这个对象被另一个对象包装起来形成包装链条。
//模拟传统语言的装饰者
//原始的飞机类
var Plan = function () {
};
Plan.prototype.fire = function () {
console.log('发射普通子弹');
};
//装饰类
var MissileDecorator = function (plan) {
this.plan = plan;
}
MissileDecorator.prototype.fire = function () {
this.plan.fire();
console.log('发射导弹!');
};
var plan = new Plan();
plan = new MissileDecorator(plan);
plan.fire();
-------------------- js中装饰者
var Plan1 = {
fire: function () {
console.log('发射普通的子弹');
}
};
var missileDecorator= function () {
console.log('发射导弹!');
};
var fire = Plan1.fire;
Plan1.fire=function () {
fire();
missileDecorator();
};
Plan1.fire();
js如何可以改变对象的扩展属性和方法,如何在不伤害原函数情况下给函数添加额外的功能?
常用就是粗暴改写函数,违背开放-封闭原则,这样很不好,可能这个函数是别人写的,你来改,这样就会显得很杂乱。
- 方法一:保存原引用方式
缺点:1. 必须维护中间变量,有可能很多,2 this指向问题。
- 方法二:AOP装饰函数
AOP (面向切面编程),缩写为Aspect Oriented Programming,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是JAVA 中Spring框架的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要作用就是把一些业务无关的功能抽离出来,例如日志打印、统计数据、数据验证、安全控制、异常处理等等。这些功能都与某些核心业务无关,但又随处可见,如果都是复制粘贴未免太没逼格,而且难以维护,不优雅。把它们抽离出来,用“动态”插入的方式嵌到各业务逻辑中。这样的好处是业务模块可以变得比较干净,不受污染,同时这些功能点能够得到很好的复用,给模块解耦。
//对window.onload的处理
window.onload=function () {
console.log('test');
};
var _onload= window.onload || function () {};
window.onload=function () {
_onload();
console.log('自己的处理函数');
};
//是新添加的函数在旧函数之前执行
Function.prototype.before=function (beforefn) {
var _this= this; //保存旧函数的引用
return function () { //返回包含旧函数和新函数的“代理”函数
beforefn.apply(this,arguments); //执行新函数,且保证this不被劫持,新函数接受的参数
// 也会被原封不动的传入旧函数,新函数在旧函数之前执行
return _this.apply(this,arguments);
};
};
//新添加的函数在旧函数之后执行
Function.prototype.after=function (afterfn) {
var _this=this;
return function () {
var ret=_this.apply(this,arguments);
afterfn.apply(this,arguments);
return ret;
};
};
应用例子有哪些?
数据上报:分离业务代码以及数据交互代码
将业务console 和下面进行数据交互,分离开来,有助于模块解耦。
var showLogin = function(){
console.log( '打开登录浮层' );
log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
console.log( '上报标签为: ' + tag );
(new Image).src = 'http:// xxx.com/report?tag=' + tag;
}
document.getElementById( 'button' ).onclick = showLogin;
--------------
var showLogin = function(){
console.log( '打开登录浮层' );
}
var log = function(){
console.log( '上报标签为: ' + this.getAttribute( 'tag' ) );
}
showLogin = showLogin.after( log ); // 打开登录浮层之后上报数据
document.getElementById( 'button' ).onclick = showLogin;
aop动态改变函数的参数
比如token,有的函数不需要有的需要,这就要求我们动态添加,保证ajax是一个纯净函数,提高它的可复用。
//给ajax请求动态添加参数的例子
var ajax=function (type,url,param) {
console.log(param);
};
var getToken=function () {
return 'Token';
};
ajax=ajax.before(function (type, url, param) {
param.token=getToken();
});
ajax('get','http://www.jn.com',{name:'zhiqiang'});
插入式表单的验证。
var validata=function () {
if(username.value===''){
alert('用户名不能为空!')
return false;
}
if(password.value===''){
alert('密码不能为空!')
return false;
}
}
var formSubmit=function () {
var param={
username=username.value;
password=password.value;
}
ajax('post','http://www.mn.com',param);
}
formSubmit= formSubmit.before(validata);
submitBtn.onclick=function () {
formSubmit();
}
装饰者模式和代理模式的区别:
-
- 代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个代替者。本体定义了关键功能,而代理提供了或者拒绝对他的访问,或者是在访问本体之前做一些额外的事情。
-
- 装饰者模式的作用就是为对象动态的加入某些行为。
ES7中装饰器
本质就是在编译执行时候的函数,装饰器对类行为的改变是在编译时不是在运行时。装饰器可以是用来对类以及方法进行修饰。
-----装饰类的静态属性
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
---------
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
--------装饰类的实例属性 类的prototype对象操作。
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass();
obj.isTestable // true
装饰器可以用来修饰函数吗?为什么?怎么做才可以?
不能,因为函数存在提升,使用高阶函数可以做到
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
------
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};
----------------
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
core-decorators.js 常用的装饰器
- @autobind:使得方法中的this对象,绑定原始对象
- @readonly:使得属性或方法不可写。
- @override:检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错
适配器模式
将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。
class Plug {
getName() {
return 'iphone充电头';
}
}
class Target {
constructor() {
this.plug = new Plug();
}
getName() {
return this.plug.getName() + ' 适配器Type-c充电头';
}
}
let target = new Target();
target.getName(); // iphone充电头 适配器转Type-c充电头
优点
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 适配对象,适配库,适配数据
缺点
- 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
- 如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善
场景
- 整合第三方SDK
- 封装旧接口
// 自己封装的ajax, 使用方式如下
ajax({
url: '/getData',
type: 'Post',
dataType: 'json',
data: {
test: 111
}
}).done(function() {})
// 因为历史原因,代码中全都是:
// $.ajax({....})
// 做一层适配器
var $ = {
ajax: function (options) {
return ajax(options)
}
}
vue的computed
<template>
<div id="example">
<p>Original message: "{{ message }}"</p> <!-- Hello -->
<p>Computed reversed message: "{{ reversedMessage }}"</p> <!-- olleH -->
</div>
</template>
<script type='text/javascript'>
export default {
name: 'demo',
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('')
}
}
}
</script>
原有data 中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式
不同点
适配器与代理模式相似
- 适配器模式:提供一个不同的接口(如不同版本的插头)
- 代理模式:提供一模一样的接口
享元模式
明确内外变化,把静态部分单独抽成对象,共享于依赖的多个对象 Fly 意为苍蝇,Flyweight 指轻蝇量级,指代对象粒度很小。
享元模式是为了解决相同对象造成的额外开销,运用共享技术支持大量细粒度对象,这个模式专注于减少开销,对数据,方法共享分离。有内部数据,内部方法和内部数据,外部方法和外部数据。这里的内部就是指相似或者共有的东西,提取出来,减少开销,提升性能。剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整 的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系 统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间 的优化模式。
性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系 统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别 通常只有男女两种,所以该内衣厂商最多只需要 2 个对象.
例子一:创建dom
// HTML
<ul id="isShow"></ul>
var arr= ['我是pm瑞瑞1','我是pm瑞瑞2','我是pm瑞瑞3','我是pm瑞瑞4'];
var oUl = document.getElementById('isShow');
for(var i=0,l=arr.length;i<l;i++){
var oLi = document.createElement('li');
oUl.appendChild(oLi);
dom.innerHTML = arr[i];
}
//享元方法
var Flyweight = function(){
var created = [];
function creat(){
var dom = document.createElement('li');
document.getElementById('isShow').appendChild(dom);
created.push(dom);
return dom;
}
return{
appendLi : function(){
if(created.length){
return created.shift();
}else{
return creat();
}
}
}
}
var arr= ['我是pm瑞瑞1','我是pm瑞瑞2','我是pm瑞瑞3','我是pm瑞瑞4'];
for(var i=0,l=arr.length;i<l;i++){
Flyweight().appendLi().innerHTML = arr[i];
}
----
//共有动作
var Flyweight = {
walkRight : function(right){
this.right = right;
},
walkLeft : function(left){
this.left = left;
}
}
var Pmrui = function(right,left){
this.right = right;
this.left = left;
}
Pmrui.prototype = Flyweight; //设置一个起始位置
var pm1 = new Pmrui(1,2);
pm1.walkRight(4);
pm1.walkLeft(5);
console.log(pm1); //{right: 4, left: 5}
例子二 驾校考试
首先假设考生的 ID 为奇数则考的是手动档,为偶数则考的是自动档。如果给所有考生都 一个驾考车,那么这个系统中就会创建了和考生数量一致的驾考车对象
var candidateNum = 10 // 考生数量
var examCarNum = 0 // 驾考车的数量
/* 驾考车构造函数 */
function ExamCar(carType) {
examCarNum++
this.carId = examCarNum
this.carType = carType ? '手动档' : '自动档'
}
ExamCar.prototype.examine = function(candidateId) {
console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}
for (var candidateId = 1; candidateId <= candidateNum; candidateId++) {
var examCar = new ExamCar(candidateId % 2)
examCar.examine(candidateId)
}
console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 10
如果考生很多,那么系统中就会存在更多个驾考车对象实例,假如驾考车对象比较复杂,那么这些新建的驾考车实例就会占用大量内存。这时我们将同种类型的驾考车实例进行合并,手动档和自动档档驾考车分别引用同一个实例,就可以节约大量内存:
var candidateNum = 10 // 考生数量
var examCarNum = 0 // 驾考车的数量
/* 驾考车构造函数 */
function ExamCar(carType) {
examCarNum++
this.carId = examCarNum
this.carType = carType ? '手动档' : '自动档'
}
ExamCar.prototype.examine = function(candidateId) {
console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}
var manualExamCar = new ExamCar(true)
var autoExamCar = new ExamCar(false)
for (var candidateId = 1; candidateId <= candidateNum; candidateId++) {
var examCar = candidateId % 2 ? manualExamCar : autoExamCar
examCar.examine(candidateId)
}
console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 2
可以看到我们使用 2 个驾考车实例就实现了刚刚 10 个驾考车实例实现的功能。这是仅有 10 个考生的情况,如果有几百上千考生,这时我们节约的内存就比较可观了,这就是享元模式要达到的目的。
如果你阅读了之前文章关于继承部分的讲解,那么你实际上已经接触到享元模式的思想了。相比于构造函数窃取,在原型链继承和组合继承中,子类通过原型 prototype 来复用父类的方法和属性,如果子类实例每次都创建新的方法与属性,那么在子类实例很多的情况下,内存中就存在有很多重复的方法和属性,即使这些方法和属性完全一样,因此这部分内存完全可以通过复用来优化,这也是享元模式的思想。
传统的享元模式是将目标对象的状态区分为内部状态 外部状态 ,内部状态相同的对象可以被共享出来指向同一个内部状态。正如之前举的驾考和四六级考试的例子中,自动档还是手动档、四级还是六级,就属于驾考考生、四六级考生中的内部状态,对应的驾考车、四六级考场就是可以被共享的对象。而考生的年龄、姓名、籍贯等就属于外部状态,一般没有被共享出来的价值。
首先创建 3 个手动档驾考车,然后注册 10 个考生参与考试,一开始肯定有 3 个考生同时上车,然后在某个考生考完之后其他考生接着后面考。为了实现这个过程,这里使用了 Promise,考试的考生在 0 到 2 秒后的随机时间考试完毕归还驾考车,其他考生在前面考生考完之后接着进行考试:
let examCarNum = 0 // 驾考车总数
/* 驾考车对象 */
class ExamCar {
constructor(carType) {
examCarNum++
this.carId = examCarNum
this.carType = carType ? '手动档' : '自动档'
this.usingState = false // 是否正在使用
}
/* 在本车上考试 */
examine(candidateId) {
return new Promise((resolve => {
this.usingState = true
console.log(`考生- ${ candidateId } 开始在${ this.carType }驾考车- ${ this.carId } 上考试`)
setTimeout(() => {
this.usingState = false
console.log(`%c考生- ${ candidateId } 在${ this.carType }驾考车- ${ this.carId } 上考试完毕`, 'color:#f40')
resolve() // 0~2秒后考试完毕
}, Math.random() * 2000)
}))
}
}
/* 手动档汽车对象池 */
ManualExamCarPool = {
_pool: [], // 驾考车对象池
_candidateQueue: [], // 考生队列
/* 注册考生 ID 列表 */
registCandidates(candidateList) {
candidateList.forEach(candidateId => this.registCandidate(candidateId))
},
/* 注册手动档考生 */
registCandidate(candidateId) {
const examCar = this.getManualExamCar() // 找一个未被占用的手动档驾考车
if (examCar) {
examCar.examine(candidateId) // 开始考试,考完了让队列中的下一个考生开始考试
.then(() => {
const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()
nextCandidateId && this.registCandidate(nextCandidateId)
})
} else this._candidateQueue.push(candidateId)
},
/* 注册手动档车 */
initManualExamCar(manualExamCarNum) {
for (let i = 1; i <= manualExamCarNum; i++) {
this._pool.push(new ExamCar(true))
}
},
/* 获取状态为未被占用的手动档车 */
getManualExamCar() {
return this._pool.find(car => !car.usingState)
}
}
ManualExamCarPool.initManualExamCar(3) // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // 10个考生来考试
如果可以将目标对象的内部状态和外部状态区分的比较明显,就可以将内部状态一致的对象很方便地共享出来,但是对 JavaScript 来说,我们并不一定要严格区分内部状态和外部状态才能进行资源共享,比如资源池模式。
上面这种改进的模式一般叫做资源池(Resource Pool),或者叫对象池(Object Pool),可以当作是享元模式的升级版,实现不一样,但是目的相同。资源池一般维护一个装载对象的池子,封装有获取、释放资源的方法,当需要对象的时候直接从资源池中获取,使用完毕之后释放资源等待下次被获取。在上面的例子中,驾考车相当于有限资源,考生作为访问者根据资源的使用情况从资源池中获取资源,如果资源池中的资源都正在被占用,要么资源池创建新的资源,要么访问者等待占用的资源被释放。资源池在后端应用相当广泛,比如缓冲池、连接池、线程池、字符常量池等场景,前端使用场景不多,但是也有使用,比如有些频繁的 DOM 创建销毁操作,就可以引入对象池来节约一些 DOM 创建损耗。
享元模式的优缺点
- 减少了系统中的对象数量,提高了程序运行效率和性能,精简了内存占用,加快运行速度;外部状态相对独立,不会影响到内部状态,所以享元对象能够在不同的环境被共享;
- 引入了共享对象,使对象结构变得复杂;共享对象的创建、销毁等需要维护,带来额外的复杂度(如果需要把共享对象维护起来的话);
适用场景
- 如果一个程序中大量使用了相同或相似对象,那么可以考虑引入享元模式;
- 如果使用了大量相同或相似对象,并造成了比较大的内存开销;
- 对象的大多数状态可以被转变为外部状态;
- 剥离出对象的外部状态后,可以使用相对较少的共享对象取代大量对象
class Contra {
constructor() {
//存储当前待执行的动作 们
this._currentstate = {};
fn: 'every'
}
createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
const aa = new Proxy(target, handler);
console.log(aa[-1]);
// this.fn = aa == '&&' ? 'every' : 'some';
return this
}
// before(beforefn) {
// var _this= this; //保存旧函数的引用
// return function () { //返回包含旧函数和新函数的“代理”函数
// beforefn.apply(this,arguments); //执行新函数,且保证this不被劫持,新函数接受的参数
// // 也会被原封不动的传入旧函数,新函数在旧函数之前执行
// return _this.apply(this,arguments);
// };
// };
//添加动作
changeState() {
this._currentstate = {};
Object.keys(arguments).forEach(
(i) => this._currentstate[arguments[i]] = true
)
return this;
}
//执行动作
contraGo() {
//当前动作集合中的动作依次执行
// return Object.keys(this._currentstate).some(
// (k) => {
// console.log(Actions[k] && Actions[k].apply(this),'1')
// return Actions[k] && Actions[k].apply(this)
// }
// )
debugger
const r = Array.prototype.call.apply(Object.keys(this._currentstate), (k) => {
return Actions[k] && Actions[k].apply(this)
})
console.log(r, 'r')
// return Object.keys(this._currentstate).every(
// (k) => {
// return Actions[k] && Actions[k].apply(this)
// }
// )
}
};
const Actions = {
up: function() {
return true;
},
down: function() {
return false;
},
forward: function() {
//向前跑
console.log('forward');
},
backward: function() {
//往老家跑
console.log('backward');
},
shoot: function() {
//开枪吧
console.log('shoot');
},
};
var littlered = new Contra();
littlered.createArray('up', 'down', '&&').changeState().contraGo();
// const result = littlered.changeState('up','down','&').contraGo();
// console.log(result)
组合模式
组合模式的特点有以下两个:
-
1 可以用来表示部分-整体的树形层次机构。
-
2 通过对象的多态性,使单对象和组合对象具有一致性。
组合模式可以非常方便地描述对象部分整体层次结构,提供了一 种遍历树形结构的方案,通过调用组合对象的 execute 方法,程序会递归调用组合对象下 面的叶对象的 execute 方法。
利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端 忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有 对象,而不需要关心它究竟是组合对象还是单个对象。
当我们添加一个命令时候不用在乎这个命令是宏命令还是子命令,我们只需要知道整个命令是个命令,并且拥有可执行的 execute 方法,
例子一:文件夹与文件
var Folder = function(name){
this.name = name;
this.files = [];
};
Folder.prototype.add = function(file){
this.files.push(file);
};
Folder.prototype.scan = function(){
console.log('开始扫描文件夹'+this.name);
for(var i=0,file,files=this.files;file=files[i++]; ){
file.scan();
}
};
var File = function(name){
this.name = name;
};
File.prototype.add = function(){
throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function(){
console.log('开始扫描文件'+this.name);
};
//创建一些文件夹和文件对象,并且让它们组合成一棵树
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('JQuery');
var file1 = new File('JavaScript1');
var file2 = new File('JavaScript2');
var file3 = new File('JavaScript3');
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
var folder3 = new Folder('Nodejs');
var file4 = new File('深入浅出');
folder3.add(file4);
var file5 = new File('123');
folder.add(folder3);
folder.add(file5);
folder.scan();
组合模式的透明性:
组合模式最大的优点在于可以一致地对待组合对象和基本对象。客户不需要知道 当前处理的是宏命令还是普通命令,只要它是一个命令,并且有 execute 方法,这个命令就可以 被添加到树中。
带来的问题是: JavaScript 中实现组合模式的难 点在于要保证组合对象和叶对象对象拥有同样的方法,这通常需要用鸭子类型的思想对它们进行 接口检查。
在 JavaScript 中实现组合模式,看起来缺乏一些严谨性,我们的代码算不上安全,但能更快 速和自由地开发,这既是 JavaScript 的缺点,也是它的优点。
问题:组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在 本质上有是区别的。组合对象可以拥有子节点,叶对象下面就没有子节点, 所以我们也许会发生一些误操作, 比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法,并且在调用这个方 法时,抛出一个异常来及时提醒客户 。
如何解决这种也对象没有子节点问题?
子节点中保存到父节点的引用。
var Folder = function(name){
this.name = name;
this.parent = null;//增加this.parent属性
this.files = [];
};
Folder.prototype.add = function(file){
file.parent = this; //设置父对象
this.files.push(file);
};
Folder.prototype.scan = function(){
console.log('开始扫描文件夹'+this.name);
for(var i=0,file,files=this.files;file=files[i++]; ){
file.scan();
}
};
Folder.prototype.remove = function(){
if(!this.parent){ //根节点或者树外的游离节点 这个文件夹要么是树的根节点,要么是还没有添加到树的游离节点,这时候没有节点需要从树中 4 移除,我们暂且让 remove 方法直接 return,表示不做任何操作。
return;
}
for(var files = this.parent.files,l = files.length;l>=0; l--){
var file = files[l];
if(file === this){
files.splice(l,1);
}
}
};
var File = function(name){
this.name = name;
this.parent = null;
};
File.prototype.add = function(){
throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function(){
console.log('开始扫描文件'+this.name);
};
File.prototype.remove = function(){
if(!this.parent){ //根节点或者树外的游离节点
return;
}
for(var files = this.parent.files,l = files.length;l>=0; l--){
var file = files[l];
if(file === this){
files.splice(l,1);
}
}
};
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var file1 = new Folder('JavaScript1');
folder1.add(new File('123'));
folder.add(folder1);
folder.add(file1);
folder1.remove();
folder.scan();
何时使用组合模式?
1 部分-整体结构:
表示对象的部分整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分整 体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最 终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模 式中增加和删除树的节点非常方便,并且符合开放封闭原则。
2 统一对待 组合对象以及单对象:
客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就 不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情, 这是组合模式最重要的能力。
对组合对象的一些理解?
1 组合模式不是父子关系
组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的。组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。组合对象包含一组叶对象,但 Leaf 并不是 Composite 的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键 7 是拥有相同的接口。
2 对叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组 叶对象的操作必须具有一致性。比如公司要给全体员工发放元旦的过节费 1000 块,这个场景可以运用组合模式,但如果公 司给今天过生日的员工发送一封生日祝福的邮件,组合模式在这里就没有用武之地了,除非先把 今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象的时候,才适合使用组 合模式。
3 双向映射关系
发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后到每个员工的邮箱里。这 本身是一个组合模式的好例子,但要考虑的一种情况是,也许某些员工属于多个组织架构。比如 某位架构师既隶属于开发组,又隶属于架构组,对象之间的关系并不是严格意义上的层次结构, 在这种情况下,是不适合使用组合模式的,该架构师很可能会收到两份过节费 . 这种复合情况下我们必须给父节点和子节点建立双向映射关系,一个简单的方法是给小组和员 工对象都增加集合来保存对方的引用。但是这种相互间的引用相当复杂,而且对象之间产生了过多 的耦合性,修改或者删除一个对象都变得困难,此时我们可以引入中介者模式来管理这些对象。
4. 用职责链模式提高组合模式性能
在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许 表现得不够理想。有时候我们确实可以借助一些技巧,在实际操作中避免遍历整棵树,有一种现 成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条,但在组合模式中,父对 象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反 过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运 用场景之一。
门面(外观)模式
应用场景
为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这个接口使子系统更加容易使用 兼容浏览器事件绑定
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
}
};
封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
场景
- 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
- 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
- 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
参考:大话设计模式
优点
- 减少系统相互依赖。
- 提高灵活性。
- 提高了安全性
缺点
- 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
行为型设计模式
- 观察者模式
- 模板模式
- 策略模式
- 职责链模式
- 状态模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 命令模式
- 解释器模式
- 中介模式
发布订阅模式
定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
发布订阅模式:
class EventEmitter {
constructor() {
// key: 事件名, value: callback[] 回调数组
this.events = {}
}
on(name, callback) {
if (typeof callback !== 'function') return console.error('请传入正确的回调函数');
if (this.events[name]) {
this.events[name].push(callback)
} else {
this.events[name] = [callback]
}
}
once(name, callback) {
if (typeof callback !== 'function') return console.error('请传入正确的回调函数');
const onceCallback = (...args) => {
callback(...args)
this.off(name)
}
this.on(name, onceCallback)
}
emit(name, ...args) {
const events = this.events[name]
if (!events) return console.warn(`${name}事件不存在`);
for (const event of events) {
event(...args);
}
}
off(name, callback) {
if (!this.events[name]) return console.warn(`${name}事件不存在`);
if (!callback) {
// 没有callback 就删除整个事件
delete this.events[name]
}
this.events[name] = this.events[name].filter(item => item !== callback)
}
}
let ob = new EventEmitter();
ob.on('add', (val) => console.log(val));
ob.emit('add', 1);
观察者模式
// 观察者
// 目标
class Observerd {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.add(observer);
}
removeObserver(observer) {
this.observers.remove(observer);
}
notify(...args) {
let obCount = this.observers.count();
for (let index = 0; index < obCount; index++) {
this.observers[i].update(...args);
}
}
}
class Observer {
constructor(doSome) {
this.doSome = doSome
}
update() {
console.log(this.doSome);
}
}
const ob1 = new Observer('我是ob1')
const ob2 = new Observer('我是ob2')
const xiaoBaiShu = new Observerd()
xiaoBaiShu.addObserver(ob1)
xiaoBaiShu.addObserver(ob2)
xiaoBaiShu.notify()
观察者模式是不是发布订阅模式?
网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。
其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。
如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。
发布订阅模式 和观察者模式区别?
- 发布者和订阅者不知道对方的存在。需要一个第三方,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。可以说是一个调度中心。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。链接
- 观察者模式中观察者和订阅者相对比较耦合,而发布-订阅模式通过提供一个调度中心,使得发布者和订阅者并不需要知道对方是谁就可以达到发布订阅的目的。
- 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的。vue中用的也是发布-订阅模式。通过一个Dep类作为调度中心。
- 观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。
模板模式
应用场景
- 在一个方法里定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
- 复用 扩展。
解决方案
abstract class Drinks {
firstStep() {
console.log('烧开水')
}
abstract secondStep()
thirdStep() {
console.log('倒入杯子')
}
abstract fourthStep()
drink() {
this.firstStep()
this.secondStep()
this.thirdStep()
this.fourthStep()
}
}
class Tea extends Drinks {
secondStep() {
console.log('浸泡茶叶')
}
fourthStep() {
console.log('加柠檬')
}
}
class Coffee extends Drinks {
secondStep() {
console.log('冲泡咖啡')
}
fourthStep() {
console.log('加糖')
}
}
// test
const tea = new Tea()
tea.drink()
const coffee = new Coffee()
coffee.drink()
策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换
<html>
<head>
<title>策略模式-校验表单</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号码:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 验证类
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(':')
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 调用代码
let registerForm = document.getElementById('registerForm')
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: 'isNoEmpty',
errorMsg: '用户名不可为空'
}, {
strategy: 'isNoSpace',
errorMsg: '不允许以空白字符命名'
}, {
strategy: 'minLength:2',
errorMsg: '用户名长度不能小于2位'
}])
validator.add(registerForm.password, [ {
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6位'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '请输入正确的手机号码格式'
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
</script>
</body>
</html>
场景例子
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 表单验证
优点
- 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
- 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
- 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点
- 会在程序中增加许多策略类或者策略对象
- 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
// 请假审批,需要组长审批、经理审批、总监审批
class Action {
constructor(name) {
this.name = name
this.nextAction = null
}
setNextAction(action) {
this.nextAction = action
}
handle() {
console.log( `${this.name} 审批`)
if (this.nextAction != null) {
this.nextAction.handle()
}
}
}
let a1 = new Action("组长")
let a2 = new Action("经理")
let a3 = new Action("总监")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
场景例子
- JS 中的事件冒泡
- 作用域链
- 原型链
优点
- 降低耦合度。它将请求的发送者和接收者解耦。
- 简化了对象。使得对象不需要知道链的结构
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任
- 增加新的请求处理类很方便。
缺点
- 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
- 使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。
状态模式
状态模式和策略模式的区别是什么?
相同点
都有一个上下文,一些策略或者状态类,上下文把请求委托给这些类来执行。
不同点
-
- 策略模式中各个策略类是平等又平行的,他们之间没有任何联系。一般用于单个算法。2. 策略模式是直接依赖注入到Context类的参数进行选择策略,不存在切换状态的操作。3. 策略主体持有算法族对象,运行时可以通过动态选择算法族中的算法(策略)来改变类的行为,策略模式实现了算法 细节可选 (即选择算法族内的算法,一个算法族包含多个可选算法)===》可以在策略库中选择算法,策略库算法无关系
-
- 状态模式中,状态和状态对应的行为早已被封装好,状态之间的切换早被规定完成。’改变行为‘这件事发生在状态模式内部。2. 每个子类中需要包含所有原来的语境类中所有方法的具体实现。3. 状态模式将各个状态所对应的操作分离开来,即对于不同的状态,由不同的子类实现具体的操作,不同状态的切换由子类实现。当发现传入的参数不是自己这个状态所对应的参数,则自己给context类切换状态 4. 状态主体(拥有者)持有状态对象,运行时可以通过动态指定状态对象来改变类的行为,状态模式实现了算法流程可变(即状态切换,不同的状态有不同的流程====》 状态不决定接下来的流程,很多流程可以选择.
解决什么问题?
有什么痛点 —— 开发过程中,因为变化、升级和维护等原因需要对原有逻辑进行修改时,很有可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有功能新测试。
怎么解决 —— 我们应该尽量通过扩展实体的行为来实现变化,而不是通过修改已有的代码来实现变化
具体一点呢 —— 类、模块和函数应该对扩展开放,对修改关闭。模块应该尽量在不修改原代码的情况下进行扩展。
核心 —— 用抽象构建框架,用实现扩展细节。
总结一下 —— 开发人员应该对程序中呈现的频繁变化的那些部分作出抽象,然后从抽象派生的实现类来进行扩展,当代码发生变化时,只需要根据需求重新开发一个实现类来就可以了。要求我们对需求的变更有一定的前瞻性和预见性,同时拒绝对于应用程序中的每个部分都刻意的进行抽象。
例子
class Contra {
constructor () {
//存储当前待执行的动作
this.lastAct = {};
}
//执行动作
contraGo (act){
if(act === 'up'){
//向上跳
}else if(act === 'forward'){
//向前冲啊
}else if(act === 'backward'){
//往老家跑
}else if(act === 'down'){
//趴下
}else if(act === 'shoot'){
//开枪
}
this.lastAct = act;
}
};
var littlered = new Contra();
littlered.contraGo('shoot');
------------------------------
function contraGo (act){
constructor () {
//存储当前待执行的动作
this.lastAct1 = "";
this.lastAct2 = "";
}
contraGo (act1, act2){
const actArr = [act1, act2];
if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('up') !== -1){
//跳着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('forward') !== -1){
//向前跑着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('down') !== -1){
//趴着开枪吧
}else if(actArr.indexOf('shoot') !== -1 && actArr.indexOf('backward') !== -1){
//回头跑着开枪吧
}else if(actArr.indexOf('up') !== -1 && actArr.indexOf('forward') !== -1){
//向前跳吧
}else if(actArr.indexOf('up') !== -1 && actArr.indexOf('down') !== -1){
//上上下下吧
}
...//等等组合
this.lastAct1 = act1;
this.lastAct2 = act2;
}
}
var littlered = new Contra();
littlered.contraGo('shoot');
大量的if else判断,加入哪天要给小红小蓝加一个回眸的动作,好嘛我又要修改contraGo方法,加一堆排列组合了,这使得contraGo成为了一个非常不稳定的方法, 而且状态越多越庞大,升华一下,contraGo方法是违反开放-封闭原则的!
使用状态模式重构
class Contra {
constructor () {
//存储当前待执行的动作 们
this._currentstate = {};
}
//添加动作
changeState (){
//清空当前的动作集合
this._currentstate = {};
//遍历添加动作
Object.keys(arguments).forEach(
(i) => this._currentstate[arguments[i]] = true
)
return this;
}
//执行动作
contraGo (){
//当前动作集合中的动作依次执行
Object.keys(this._currentstate).forEach(
(k) => Actions[k] && Actions[k].apply(this)
)
return this;
}
};
const Actions = {
up : function(){
//向上跳
console.log('up');
},
down : function(){
//趴下
console.log('down');
},
forward : function(){
//向前跑
console.log('forward');
},
backward : function(){
//往老家跑
console.log('backward');
},
shoot : function(){
//开枪吧
console.log('shoot');
},
};
var littlered = new Contra();
littlered.changeState('shoot','up').contraGo(); / /shoot up
状态模式,将条件判断的结果转化为状态对象内部的状态(代码中的up,down,backward,forward),内部状态通常作为状态对象内部的私有变量(this._currentState),然后提供一个能够调用状态对象内部状态的接口方法对象(changeState,contraGo),这样对状态的改变,对状态方法的调用的修改和增加也会很容易,方便了对状态对象中内部状态的管理。
同时,状态模式将每一个条件分支放入一个独立的类中,也就是代码中的Actions。这使得你可以根据对象自身的情况将对象的状态(动作——up,down,backward,forward)作为一个对象(Actions.up,Actions.down这样),这一对象可以不依赖于其他对象而独立变化(一个行为一个动作,互不干扰)。
状态模式就是一种适合多种状态场景下的设计模式,改写之后代码更加清晰,提高代码的维护性和扩展性,不用再牵一发动全身。
使用场景
- 一个由一个或多个动态变化的属性导致发生不同行为的对象,在与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化,那么这个对象,就是有状态的对象
- 代码中包含大量与对象状态有关的条件语句,像是if else或switch case语句,且这些条件执行与否依赖于该对象的状态。
状态模式的优缺点
优点:
- 一个状态状态对应行为,封装在一个类里,更直观清晰,增改方便
- 状态与状态间,行为与行为间彼此独立互不干扰
- 避免事物对象本身不断膨胀,条件判断语句过多
- 每次执行动作不用走很多不必要的判断语句,用哪个拿哪个
缺点:
- 需要将事物的不同状态以及对应的行为拆分出来,有时候会无法避免的拆分的很细,有的时候涉及业务逻辑,一个动作拆分出对应的两个状态,动作就拆不明白了,过度设计
- 必然会增加事物类和动作类的个数,有时候动作类再根据单一原则,按照功能拆成几个类,会反而使得代码混乱,可读性降低
const States = {
"show": function () {
console.log("banner展现状态,点击关闭");
this.setState({
currentState: "hide"
})
},
"hide": function () {
console.log("banner关闭状态,点击展现");
this.setState({
currentState: "show"
})
}
};
同样通过一个对象States来定义banner的状态,这里有两种状态show和hide,分别拥有相应的处理方法,调用后再分别把当前banner改写为另外一种状态。接下来来看导航类Banner:
class Banner extends Component{
constructor(props) {
super(props);
this.state = {
currentState: "hide"
}
this.toggle = this.toggle.bind(this);
}
toggle () {
const s = this.state.currentState;
States[s] && States[s].apply(this);
}
render() {
const { currentState } = this.state;
return (
<div className="banner" onClick={this.toggle}>
</div>
);
}
};
export default Banner;
迭代器模式
提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
class Iterator {
constructor(conatiner) {
this.list = conatiner.list
this.index = 0
}
next() {
if (this.hasNext()) {
return this.list[this.index++]
}
return null
}
hasNext() {
if (this.index >= this.list.length) {
return false
}
return true
}
}
class Container {
constructor(list) {
this.list = list
}
getIterator() {
return new Iterator(this)
}
}
// 测试代码
let container = new Container([1, 2, 3, 4, 5])
let iterator = container.getIterator()
while(iterator.hasNext()) {
console.log(iterator.next())
}
场景例子
- Array.prototype.forEach
- jQuery中的$.each()
- ES6 Iterator
特点
- 访问一个聚合对象的内容而无需暴露它的内部表示。
- 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作
总结
对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式
访问者模式
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
// 访问者
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()
element.accept(visitor)
场景例子
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
优点
- 符合单一职责原则
- 优秀的扩展性
- 灵活性
缺点
- 具体元素对访问者公布细节,违反了迪米特原则
- 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
- 具体元素变更比较困难
备忘录模式
应用场景
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
//备忘类
class Memento{
constructor(content){
this.content = content
}
getContent(){
return this.content
}
}
// 备忘列表
class CareTaker {
constructor(){
this.list = []
}
add(memento){
this.list.push(memento)
}
get(index){
return this.list[index]
}
}
// 编辑器
class Editor {
constructor(){
this.content = null
}
setContent(content){
this.content = content
}
getContent(){
return this.content
}
saveContentToMemento(){
return new Memento(this.content)
}
getContentFromMemento(memento){
this.content = memento.getContent()
}
}
//测试代码
let editor = new Editor()
let careTaker = new CareTaker()
editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')
console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333
editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
场景例子
- 分页控件
- 撤销组件
优点
给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
缺点
消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
命令模式
命令模式中的命令(command)指的是一个执行某些 特定事情的指令。
命令模式最常见的应用场景是: 有时候需要向某些对象发送请求,但是并不知道请求的接收 者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
点击了按钮之后,必须向某些负责具体行为 的对象发送请求,这些对象就是请求的接收者。但是目前并不知道接收者是什么对象,也不知道 接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之 间的耦合。
设计模式的主题总是把不变的事物和变化的事物分离开来,命令模式也不例外。按下按钮之 后会发生一些事情是不变的,而具体会发生什么事情是可变的。通过 command 对象的帮助,将来 我们可以轻易地改变这种关联,因此也可以在将来再次改变按钮的行为 。
<body>
<button id="button1">点击按钮 1</button>
<button id="button2">点击按钮 2</button>
<button id="button3">点击按钮 3</button>
</body>
<script>
var button1 = document.getElementById( 'button1' ),
var button2 = document.getElementById( 'button2' ),
var button3 = document.getElementById( 'button3' );
</script>
暴露命令的接口:
var setCommand = function(button,command){
button.onclick = function(){
command.execute();
}
};
命令接收者:
var MenuBar = {
refresh: function(){
console.log('刷新菜单目录' )
}
}
var SubMenu = {
add: function(){
console.log('增加子菜单')
},
del: function(){
console.log('删除子菜单' );
}
};
将行为封装到命令类:
var RefreshMenuBarCommand = function(receiver){
this.receiver = receiver;
};
RefreshMenuBarCommand.prototype.execute = function(){
this.receiver.refresh();
};
var AddSubMenuCommand = function(receiver){
this.receiver = receiver;
};
AddSubMenuCommand.prototype.execute = function(){
this.receiver.add();
};
var DelSubMenuCommand = function(receiver){
this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function(){
console.log('删除子菜单');
};
最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1,refreshMenuBarCommand);
setCommand(button2,addSubMenuCommand);
setCommand(button3,delSubMenuCommand);
由上可以看出,命令模式具有几个角色,有 execute 方法名,command 对象 , receiver 命令接收者, 不用命令模式也可以完成这些任务,命令模 式将过程式的请求调用封装在 command 对象的 execute 方法里,通过封装方法调用,我们可以把 运算块包装成形。command 对象可以被四处传递,所以在调用命令的时候,客户(Client)不需要 关心事情是如何进行的。
命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。运算块不一定要封装在 command.execute 方法中,也可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。即使我们依然需要请求“接收者”,那也未必使用 面向对象的方式,闭包可以完成同样的功能。
在面向对象设计中,命令模式的接收者被当成 command 对象的属性保存起来,同时约定执行 命令的操作调用 command.execute 方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产 生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象 的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用 闭包实现的命令模式如下代码所示:
var setCommand = function( button, func ){
button.onclick = function(){
func();
}
};
var MenuBar = {
refresh: function(){
console.log( '刷新菜单界面' );
}
};
var RefreshMenuBarCommand = function( receiver ){
return function(){
receiver.refresh();
}
};
var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
setCommand( button1, refreshMenuBarCommand );
为了扩展现在使用的是命令模式,或者扩展其他的命令,最好加上execute关键字。
var refreshMenuBarCommand = function(receiver){
return{
execute: function(){
receiver.refresh();
}
}
};
var setCommand = function(button,command){
button.onclick = function(){
command.execute();
}
};
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
setCommand(button1,refreshMenuBarCommand);
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令 。
var closeDoorCommand = {
execute: function(){
console.log('关门');
}
};
var openPcCommand = {
execute: function(){
console.log('开电脑');
}
};
var openQQCommand = {
execute: function(){
console.log('登录QQ');
}
};
var MacroCommand = function(){
return {
commandsList:[],
add: function(){
for(var i=0,command;command = this.commandsList[i++]; ){
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();
命令模式都会在 command 对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者来执行,这种模式的好 处是请求发起者和请求接收者之间尽可能地得到了解耦。
但是我们也可以定义一些更“聪明”的命令对象,“聪明”的命令对象可以直接实现请求, 这样一来就不再需要接收者的存在,这种“聪明”的命令对象也叫作智能命令。没有接收者的智 能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图 的不同。策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标 的不同手段,它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广,command 对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能。
解释器模式
应用场景
给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
class Context {
constructor() {
this._list = []; // 存放 终结符表达式
this._sum = 0; // 存放 非终结符表达式(运算结果)
}
get sum() {
return this._sum;
}
set sum(newValue) {
this._sum = newValue;
}
add(expression) {
this._list.push(expression);
}
get list() {
return [...this._list];
}
}
class PlusExpression {
interpret(context) {
if (!(context instanceof Context)) {
throw new Error("TypeError");
}
context.sum = ++context.sum;
}
}
class MinusExpression {
interpret(context) {
if (!(context instanceof Context)) {
throw new Error("TypeError");
}
context.sum = --context.sum;
}
}
/** 以下是测试代码 **/
const context = new Context();
// 依次添加: 加法 | 加法 | 减法 表达式
context.add(new PlusExpression());
context.add(new PlusExpression());
context.add(new MinusExpression());
// 依次执行: 加法 | 加法 | 减法 表达式
context.list.forEach(expression => expression.interpret(context));
console.log(context.sum);
优点
- 易于改变和扩展文法。
- 由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
缺点
- 执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度慢
- 对于复杂的文法比较难维护
中介模式
应用场景
中介者对象可以让各个对象之间不需要显示的相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互。
打个比方,军火买卖双方为了安全起见,找了一个信任的中介来进行交易。买家A把钱交给中介B,然后从中介手中得到军火,卖家C把军火卖给中介,然后 从中介手中拿回钱。一场交易完毕,A甚至不知道C是一只猴子还是一只猛犸。因为中介的存在,A也未必一定要买C的军火,也可能是D,E,F。
银行在存款人和贷款人之间也能看成一个中介。存款人A并不关心他的钱最后被谁借走。贷款人B也不关心他借来的钱来自谁的存款。因为有中介的存在,这场交易才变得如此方便。
中介者模式和代理模式有一点点相似。都是第三者对象来连接2个对象的通信。具体差别可以从下图中区别。
代理模式:
中介者模式:代理模式中A必然是知道B的一切,而中介者模式中A,B,C对E,F,G的实现并不关心.而且中介者模式可以连接任意多种对象。
切回到程序世界里的mvc,无论是j2ee中struts的Action. 还是js中backbone.js和spine.js里的Controler. 都起到了一个中介者的作用.
拿backbone举例. 一个mode里的数据并不确定最后被哪些view使用. view需要的数据也可以来自任意一个mode. 所有的绑定关系都是在controler里决定. 中介者把复杂的多对多关系, 变成了2个相对简单的1对多关系
var mode1 = Mode.create(), mode2 = Mode.create();
var view1 = View.create(), view2 = View.create();
var controler1 = Controler.create( mode1, view1, function(){
view1.el.find( ''div' ).bind( ''click', function(){
this.innerHTML = mode1.find( 'data' );
} )
})
var controler2 = Controler.create( mode2 view2, function(){
view1.el.find( ''div' ).bind( ''click', function(){
this.innerHTML = mode2.find( 'data' );
} )
})