js常用设计模式

88 阅读6分钟

《设计模式》一书中对设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。但前端JS开发中,并不是经常使用面向对象的软件设计方式,因此常说的23种设计模式并不适合生搬硬套到前端。“设计模式”也没有那么高深,可以简单理解为设计模式就是在软件开发过程中针对一些通用问题的好的代码设计方案。
总结一下前端常用的设计模式

发布订阅模式

  • 发布订阅模式,又叫观察者模式,它定义了对象间的一种一对多的依赖关系,一个对象状态发生变化时,所有订阅它的对象(观察者)都会得到通知。
  • 发布订阅模式解耦了发布者和订阅者之间的耦合关系:发布者不用关心订阅者是谁,也不用关心订阅者的数量,只需要发布消息,所有订阅者都会收到通知;订阅者也不用关心发布者是谁。
  • 此外,发布订阅模式在时间上进行了解耦,订阅者不用关心什么时候发布,只需要监听发布事件,发布时会得到通知。
  • 发布订阅模式广泛应用于异步编程当中,前端的DOM事件、异步回调函数本质上都是发布订阅。
// 一个基础的发布订阅器
const Event = (function(){
	const clientList = {};
	function $on(key, fn) {
		clientList[key] = clientList[key] || [];
		clientList[key].push(fn);
	}
	function $emit(key, ...args) {
		const listeners = clientList[key];
		if(!listeners || !listeners.length) { return }
		listeners.forEach(listener => listener(...args))
	}
	function $off(key, fn) {
		const listeners = clientList[key]
		if(!listeners) {
			return;
		}
		if(!fn) {
			listeners.length = 0;
		} else {
			const len = listeners.length;
			for (let i = len-1; i >= 0; i--) {
				if(fn === listeners[i]) {
					listeners.splice(i, 1);
				}
			}
		}
	}

	return {
		$on,
		$emit,
		$off
	}
}())

代理模式

  • 当不方便直接访问一个对象时,提供一层代理,控制对这个对象的访问,访问的实际上是中间的代理对象。
  • 代理可以帮助本体过滤掉不符合要求的请求,保护本体被攻击
  • 如果访问本体的成本很高,代理可以选择在合适的时候访问本体,减少成本
  • 代理还可以做缓存
  • 代理和本体的接口最好保持一致
// 使用代理模式创建单例类
function Singleton(name) {
	this.name = name;
}
const ProxySingleton = (function() {
	let instance;
	return function(...args) {
		if(!instance) {
			instance = new Singleton(...args);
		}
		return instance;
	}
})();

// 图片懒加载
const lazyLoadImg = (function() {
	const imgNode = document.createElement('img');
	const img = new Image();
	document.body.appendChild(imgNode);
	const tempImgUrl = './img/temp.jpg';

	const setSrc = (src) => {
		imgNode.src = tempImgUrl;
		img.src = src;
	}

	img.onload = () => {
		imgNode.src = img.src;
	}

	return {
		setSrc
	}
})();

// 使用代理实现图片懒加载
// 将设置图片链接地址和懒加载功能分离开,代码更符合高内聚低耦合
const loadImg = (function() {
	const imgNode = document.createElement('img');
	document.body.appendChild(imgNode);

	const setSrc = (src) => {
		imgNode.src = src;
	}

	return {
		setSrc
	}
})();

const lazyLoadImg = (setSrc) => {
	const img = new Image();
	const tempImgUrl = './img/temp.jpg';

	const setSrc = (src) => {
		loadImg.setSrc(tempImgUrl);
		img.src = src;
	}

	img.onload = () => {
		loadImg.setSrc(img.src);
	}

	return {
		setSrc
	}
}

// 缓存代理
function mult(...args) {
	let a = 1;
	return args.reduce((pre, cur) => (pre * cur), a);
}

const cacheMult = (function() {
	const cache = new Map();
	return function(...args) {
		let res = cache.get(args.join(''));
		if(!res) {
			res = mult(...args);
			cache.set(args.join(''), res);
		}
		return res;
	}
})();

单例模式

基本思想: 控制一个类只能产生一个实例。例如Vue根实例、VueX状态管理仓库都是单例。
思路:
1、使用一个变量判断这个类是否被实例化过
2、提供一个访问api

// 函数写法
function Singleton(name) {
	this.name = name;
}

Singleton.getInstance = (function() {
	let instance;
	return function(name) {
		if(!instance) {
			instance = new Singleton(name);
		}
		return instance;
	}
})();

// 类的写法
class SingleTon {
	constructor(name) {
		this.name = name;
	}
	getName() {
		return this.name
	}
	static getSingleton = (function() {
		let instance
		return function(...args) {
			if(!instance) {
				instance = new SingleTon(...args);
			}
			return instance
		}
	}());
}

// 透明单例——劫持new,让访问api跟普通的new调用一样
const Singleton = (function() {
	let instance
	return function(name) {
		if(!instance) {
			instance = this;
		}
		this.name = name;
		return instance;
	}
})();

// 透明单例劫持了new,却将实例初始化和控制只产生一个实例的逻辑耦合在了一起,使用代理解耦
// 新增一个代理类,将控制只产生一个实例的能力放到代理类中
function Singleton(name) {
	this.name = name;
}

const ProxySingleton = (function() {
	let instance;
	return function(...args) {
		if(!instance) {
			instance = new Singleton(...args);
		}
		return instance;
	}
})();

// 将代理类通用
const getSingletonClass = function(fn) {
	let instance;
	return function(...args) {
		if(!instance) {
			instance = new fn(...args);
		}
		return instance;
	}
}

策略模式

例子:小明去北京,可以选择坐飞机、火车或自驾,飞机、火车、自驾 是不同的策略,它们都能将小明送去北京,它们是等价的。
策略模式指的是定义一系列策略算法,并将这些策略算法都单独封装起来(策略类),这些策略算法是可以互相替换的,具有相同的输入数据接口;然后定义一个Context,负责将请求委托给具体的策略类,由策略类负责具体的计算和实现。
策略类解耦了策略算法的使用和实现,在js中策略模式已经融入了语言本身,很多时候是隐形的,常用的场景有表单验证,将不同的校验规则定义成一个个策略算法
策略模式可以避免多重if-else条件语句,符合开放-封闭原则,便于策略的拓展

// 计算年终奖

// 最粗暴的方式
function calculateBonus(salary, performanceLevel) {
	if (performanceLevel === 'S') {
		return salary * 8;
	}
	if (performanceLevel === 'A') {
		return salary * 6;
	}
	if (performanceLevel === 'B') {
		return salary * 4;
	}
}

// 好一点的方式
const PERFORMANCE_LEVEL = {
	'S': 8,
	'A': 6,
	'B': 4
}

function myCalculateBonus(salary, performanceLevel) {
	return salary * PERFORMANCE_LEVEL[performanceLevel];
}

// 推荐的js中的策略模式
// 对比自己刚开始的实现,好处是方便拓展不同绩效的计算方式,但是这个策略对象会越来越大
const strategies = {
	'S': function(salary) {
		return salary * PERFORMANCE_LEVEL['S'];
	},
	'A': function(salary) {
		return salary * PERFORMANCE_LEVEL['A'];
	},
	'B': function(salary) {
		return salary * PERFORMANCE_LEVEL['B'];
	}
}

function calculateBouns(salary, performanceLevel) {
	return strategies[performanceLevel](salary);
}

职责链模式

// const order500 = function (type, pay, stock) {
// 	if(type === 1 && pay) {
// 		console.log('500元定金预购')
// 	} else {
// 		order300(type, pay, stock);
// 	}
// }

// const order300 = (type, pay, stock) => {
// 	if(type === 2 && pay) {
// 		console.log('300元定金预购')
// 	} else {
// 		orderNormal(type, pay, stock);
// 	}
// }

// const orderNormal = (type, pay, stock) => {
// 	if(type === 3 && pay && stock > 0) {
// 		console.log('普通购买')
// 	} else {
// 		console.log('库存不足')
// 	}
// }

const NEXT_FLAG = 'nextSuccessor';

const order500 = function (type, pay, stock) {
	if(type === 1 && pay) {
		return '500元定金预购';
	} else {
		return NEXT_FLAG;
	}
}

const order300 = function (type, pay, stock) {
	if(type === 2 && pay) {
		return '300元定金预购';
	} else {
		return NEXT_FLAG;
	}
}

const orderAsync = function (type, pay, stock) {
	setTimeout(() => {
		if(type === 4 && pay) {
			return '异步执行';
		} else {
			// return NEXT_FLAG;  此处异步return,excute中同步代码获取不到返回值
			return this.next(type, pay, stock);
		}
	}, 1000);
}

const orderNormal = (type, pay, stock) => {
	if(type === 3 && pay && stock > 0) {
		return '普通购买';
	} else {
		return '库存不足';
	}
}

const Chain = function(fn) {
	this.fn = fn;
	this.nextSuccessor = null;
}

Chain.prototype.setNextSuccessor = function(fn) {
	return this.nextSuccessor = fn;
}

Chain.prototype.execute = function(...args) {
	const ret = this.fn.apply(this, args);
	if(ret === NEXT_FLAG) {
		return this.nextSuccessor.execute(...args);
	}
	return ret;
}

Chain.prototype.next = function(...args) {
	return this.nextSuccessor.execute(...args);
}

const chainOrder500 = new Chain(order500);
const chainOrder300 = new Chain(order300);
const chainOrderAsync = new Chain(orderAsync);
const chainOrderNormal = new Chain(orderNormal);

chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrderAsync);
chainOrderAsync.setNextSuccessor(chainOrderNormal);

console.log(chainOrder500.execute(1, true, 100));
console.log(chainOrder500.execute(2, true, 0));
console.log(chainOrder500.execute(2, false, 300));
console.log(chainOrder500.execute(3, true, 300));
console.log(chainOrder500.execute(4, true, 100));
console.log(chainOrder500.execute(3, true, 100));

Function.prototype.after = function(fn) {
	let self = this;
	return function() {
		const ret = self.apply(this, arguments);
		if(ret === NEXT_FLAG) {
			return fn.apply(this, arguments);
		}
		return ret;
	}
}

const myorder = order500.after(order300).after(orderNormal);

console.log(myorder(3, true, 10));

命令模式

    var TV = {
      open() {
        console.log('open tv');
      },
      close() {
        console.log('close tv');
      }
    }

    var OpenTVCommand = function (reciver) {
      this.reciver = reciver
    }

    OpenTVCommand.prototype.execute = function() {
      this.reciver.open();
    }

    OpenTVCommand.prototype.undo = function() {
      this.reciver.close();
    }

    var createCommand = function(reciver) {
      function execute() {
        reciver.open()
      }
      function undo() {
        reciver.close()
      }

      return {
        execute,
        undo
      }
    }

    var setCommand = function(obj) {
      document.getElementById('execute').onclick = function() {
        obj.execute();
      }
      document.getElementById('undo').onclick = function() {
        obj.undo();
      }
    }

    // setCommand(new OpenTVCommand(TV));
    setCommand(createCommand(TV));