基础知识
编程语言按照数据类型来区分的话是分为静态类型语言和动态类型语言。而JavaScript是一门典型的动态语言,我们对变量进行赋值的时候不用考虑其类型,所以JavaScript具有很大的灵活性。
多态
多态的含义: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
其实在JavaScript中多态很容易实现,下面演示一段‘多态‘的代码:
class Duck {
}
class Dog {
}
const dog = new Dog();
const duck = new Duck()
function makeSound(animal) {
if (animal instanceof Duck) {
console.log('嘎嘎嘎');
} else if (animal instanceof Dog) {
console.log('wangwangwang');
}
}
makeSound(duck);
makeSound(dog)
// 嘎嘎嘎
// wangwangwang
上面的代码确实是体现了多态,传入不同的参数反应出不同的结果,但是其内部实现确实有点问题,当我们又添加了一个动物,我们需要修改makeSound中的代码,如果加了很多动物,那该函数内部的if else也会增加,所以我们可以把不变的保留,变得部分提取出去。
优化后的代码:
class Duck {
sound() {
console.log('gagaga');
}
}
class Dog {
sound() {
console.log('wangwangwang')
}
}
const dog = new Dog();
const duck = new Duck()
function makeSound(animal) {
animal.sound()
}
makeSound(duck);
makeSound(dog)
// gagaga
// wangwangwang
将各个动物叫的方法封装到自身上,然后makeSound只会调用sound方法,只要有该方法的对象都可以穿入makeSound。这样我们只需要添加一个动物并实现sound方法,并不需要去修改makeSound了。这样就消除了分支语句。
函数currying和uncurrying
currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
当我们需要对一个函数的参数做分步的处理就可以使用该方式。
const currying = function (fn) {
console.log();
const length = fn.length;
return function (...args) {
console.log(arguments);
if (args.length < length) {
return currying(fn.bind(this, ...args));
} else {
return fn.apply(this, args)
}
}
}
function MyCurrying(fn) {
function curried(...args) {
// 判断接收参数个数
if (args.length >= fn.length) {
return fn.apply(this, args)
} else {
// 没有达到参数个数要求就返回函数继续接收参数,递归调用
function curried2(...args2) {
return curried.apply(this, [...args, ...args2])
}
return curried2
}
}
return curried
}
function sum(a, b, c, d) {
return `[${a}]-[${b}]:${c}:${d}`
}
const currySum = currying(sum);
console.log(currySum(1, 2, 3)(4));
console.log(currySum(1, 2)(3, 4));
const waitSum = currySum('feat', 'zhangsan');
console.log(waitSum('添加功能', '添加了'));
console.log(waitSum('修改字段')('修改失败'));
在JavaScript中一个对象也未必只能使用它自身的方法,我们可以通过call/apply来实现这一功能
function foo(...args) {
Array.prototype.push.call(args, 4);
console.log(args);
}
foo(1, 2, 3, 4, 5);
// [ 1, 2, 3, 4, 5, 4 ]
现在我们吧this泛化的过程提取出来,我们就可以使用uncurrying来实现,下面是其实现的方式之一:
Function.prototype.uncurrying = function () {
const self = this;
return function (...args) {
// 截取第一个值并返回第一个参数
const obj = Array.prototype.shift.call(args);
return self.apply(obj, args);
}
}
接下来我们改造下之前的代码:
const push = Array.prototype.push.uncurrying();
function foo(...args) {
push(args, 4);
console.log(args);
}
foo(1, 2, 3, 4, 5);
// 1,2,3,4,5,4
现在我们可以看出,我们将数组push方法改造为了一个通用的方法。每次写就不用写那么长的原型了。uncurrying还有另外一种实现:
Function.prototype.uncurrying = function () {
const self = this;
return function (...args) {
// 截取第一个值并返回该元素
return Fn = Function.prototype.call.apply(self, args);
}
}
分时函数
在渲染大量的DOM的时候,如果一次性加载的问题,浏览器可能会卡住。这塞我们需要调整渲染方式,让节点在一定的时间加载一定。
注意在浏览器中测试。
// 生成数组
const ary = [];
for (let i = 0; i <= 1000; i++) {
ary.push(i);
}
// 设置渲染
const renderList = function (data) {
for (let i = 0, l = data.length; i < l; i++) {
}
}
// renderList(ary)
function timeChunk(data, fn, count) {
let obj, t;
const len = data.length;
// 一次性需创建的个数
const start = function () {
for (let i = 0; i < Math.min(count || 1, data.length); i++) {
const obj = data.shift();
fn(obj);
}
}
return function () {
t = setInterval(() => {
if (data.length === 0) {
return clearInterval(t)
}
start();
}, 200)
}
}
const optRenderList = timeChunk(ary, (obj) => {
const div = document.createElement('div');
div.innerHTML = obj;
div.className = 'box'
document.body.appendChild(div);
}, 10)
optRenderList();
惰性加载函数
在浏览器中我们可能要对一些方法做一些兼容性的处理。
let addEvent = function (ele, type, handler) {
if (window.addEventListener) {
addEvent = function (ele, type, handler) {
ele.addEventListener(type, handler, false)
}
} else if (window.attachEvent) {
addEvent = function (ele, type, handler) {
ele.attachEvent('on' + type, handler);
}
}
addEvent(ele, type, handler);
}
const box1 = document.querySelector('.box1');
box1.onclick = debounce(function () {
console.log('点击了')
}, 300)
addEvent(box1, 'click', function () {
console.log('惰性加载');
})
上述做到了使用的时候重写函数,保证性能的优化,而不需要一开始加载代码的时候就去判断兼容性。
设计模式
原型模式
一些概念
原型模式不单单是一个设计模式,也是一种编程范式。其实现的关键是语言本身是否提供了clone方法。
首先JavaScript就是一门基于原型模式和原型继承的一门语言,其面向对象的系统就是基于原型来构建的。
原型模式在设计模式的角度看是创建对象的一种方式。原型模式通过克隆来创建对象。原型模式实现的关键在于语言本身是否提供clone,例如ES5的Object.create方法。
当然其兼容的实现方法为:
function create(obj) {
function Fn(){};
Fn.prototype = obj;
return new Fn();
}
另外要注意,原型模式的克隆并不是要得到一个完全一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。
原型继承
在实现原型继承方面,JavaScript遵循以下原型编程的规则:
- 所有的数据都是对象。
- 得到一个对象并不是通过实例化类,而是使用一个对象作为原型去克隆得到一个新对象。
- 对象是能访问其原型的。
- 而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段(原型链)。
JavaScript中的根对象是Object.prototype对象。Object.prototype对象是一个空的对象。我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype对象就是它们的原型。
在JavaScript中虽然我们可以通过new 构造函数的方式来实例化一个对象,但是该语言中并没有类的概念,而且去分析new的过程也会发现实例化的过程也是使用的原型去克隆。就算是ES6新出的class语法使得JavaScript看起来像传统的基于类的语言了,但实际上本质还是基于原型机制创建对象,class仅仅只是一个语法糖而已。
总结
这里以JavaScript语言面对对象的系统设计来简要介绍了原型模式。其实这也证实了一句话:设计模式其实是对一门语言不足的一个补充,如果要使用设计模式不如去寻找更好的一门语言。当然随着编程语言的不断发展,一些设计模式可能天然就内置了,不再需要强行模拟实现。
单例模式
定义:保证一个类只有一个实例,并提供一个可访问它的全局访问点。
实现一个单例模式
以下是一个单例模式的基本实现,原理就是第一次创建一个实例,并保存该实例的引用,第二次想创建的时候就直接返回第一次创建的实例引用。
class SingleDog {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name
}
static instance = null;
static getInstance(name, age) {
if (!this.instance) {
this.instance = new SingleDog(name, age);
}
return this.instance;
}
}
const dog = SingleDog.getInstance('lala', 11);
const dog2 = SingleDog.getInstance('qiqi', 11);
console.log(dog, dog2, dog === dog2);
透明的单例模式
之前实现的单例模式虽然功能上没问题,但是在使用上却与我们一般创建对象的方式不一样,用户必须要知道有一个专门创建单例的方法。接下来进行一些改造。
class CreateDiv {
constructor(html) {
if (CreateDiv.instance) {
return CreateDiv.instance
}
this.html = html;
this.init();
CreateDiv.instance = this;
return this;
}
init() {
const div = document.createElement('div');
div.innerHTML = this.html;
document.body.appendChild(div);
}
}
const a = new CreateDiv('hello world');
const b = new CreateDiv('你好世界');
console.log(a === b);
代理实现单例模式
之前的透明单例使得用法上统一了,但是加入有一天我们想利用该类来创建千万个对象,那我们又必须去修改类内部来支持创建多个对象。这是非常不灵活的。所以我们可以将创建单例的这个行为单独用一个代理类来实现。
class CreateDiv {
constructor(html) {
this.html = html;
this.init();
}
init() {
const div = document.createElement('div');
div.innerHTML = this.html;
document.body.appendChild(div)
}
}
class ProxySingletonCreateDiv {
static instance;
constructor(html) {
if (!ProxySingletonCreateDiv.instance) {
ProxySingletonCreateDiv.instance = new CreateDiv(html)
}
return ProxySingletonCreateDiv.instance;
}
}
const a = new ProxySingletonCreateDiv('1');
const b = new ProxySingletonCreateDiv('2');
console.log(a === b);
这样我们以后像创建多个对象的时候就直接使用CreateDiv即可,想创建单个对象就使用代理类。
JavaScript中的单例模式
上面的介绍其实是通过传统的语言模式来介绍单例模式,但是JavaScript严格意义上并无类这个概念,所以传统的单例模式在JavaScript中并不适用。
我们可以直接通过对象字面量形式创建一个对象:
// 这个对象是独一无二的
const a = {};
惰性单例
惰性单例顾名思义就是使用的时候才去创建。
接下来以一个弹窗案例来体现:
// 抽离创建单例的逻辑
const getSingle = function (fn) {
let res;
return function (...args) {
return res || (res = fn.apply(this, args));
}
}
const createLoginLayer = function () {
const div = document.createElement('div');
div.innerHTML = '我是登陆浮窗'
div.style.display = 'none'
document.body.appendChild(div);
return div;
}
const createIframeLayer = function () {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return iframe
}
const createSingletonLoginLayer = getSingle(createLoginLayer);
const createsingletonIframeLayer = getSingle(createIframeLayer)
document.querySelector('.btn1').onclick = function () {
const loginLayer = createSingletonLoginLayer();
const iframeLayer = createsingletonIframeLayer();
loginLayer.style.display = 'block'
iframeLayer.src = 'https://altria.topzhang.cn'
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 100px;
height: 100px;
background-color: #bfa;
text-align: center;
line-height: 100px;
margin: 10px;
}
</style>
</head>
<body>
<button class="btn1">点击创建</button>
<script src="./4-惰性单例.js"></script>
</body>
</html>
总结
单例模式在JavaScript中也涉及到了闭包和高阶函数的概念,而且单例模式虽然简单,但十分的有用。
策略模式
定义:定义一系列的算法,把它们一个一个的封装起来并且使用的时候可随意替换。
奖金计算
员工根据绩效来确定其奖金是多少,比如S绩效四倍工资,A绩效三倍工资等。
// 奖金根据绩效来计算,每个等级的绩效创建一个类
class PerformanS {
calculate(salary) {
return salary * 4
}
}
class PerformanA {
calculate(salary) {
return salary * 3
}
}
class PerformanB {
calculate(salary) {
return salary * 2
}
}
class PerformanC {
calculate(salary) {
return salary * 1
}
}
class Bonus {
// 设置薪水
setSalary(salary) {
this.salary = salary
}
// 设置策略对象
setStrategy(strategy) {
this.strategy = strategy
}
getBonus() {
return this.strategy.calculate(this.salary)
}
}
const c1 = new Bonus();
c1.setSalary(10000);
c1.setStrategy(new PerformanS());
const c2 = new Bonus();
c2.setSalary(3000);
c2.setStrategy(new PerformanC());
const c3 = new Bonus();
c3.setSalary(8000);
c3.setStrategy(new PerformanA());
const c4 = new Bonus();
c4.setSalary(6666);
c4.setStrategy(new PerformanB());
console.log(c1.getBonus());
console.log(c2.getBonus());
console.log(c3.getBonus());
console.log(c4.getBonus());
JavaScript版奖金计算
上述的奖金计算其实是传统的面向对象基于类的方法实现,但是JavaScript测非常的灵活可直接按照以下方式实现:
// 之前的计算是基于传统语言的策略模式,js版要更加灵活些
const strategies = {
S(salary) {
return salary * 4
},
A(salary) {
return salary * 3
},
B(salary) {
return salary * 2
},
C(salary) {
return salary * 1
}
}
const calculateBonus = function (level, salary) {
return strategies[level](salary);
}
console.log(calculateBonus('S', 2000));
console.log(calculateBonus('A', 1000));
实现小球动画
// 首先线了解些缓动算法,
/*
* t: current time(当前时间);当前时刻-开始时刻
* b: beginning value(初始值);初始位置
* c: change in value(变化量);目标位置-变化量
* d: duration(持续时间)。
*/
const tween = {
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
easeOut: function (t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
},
easeInOut: function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
},
expoEaseIn: function (t, b, c, d) {
return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
},
expoEaseOut: function (t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
},
expoEaseInOut: function (t, b, c, d) {
if (t == 0) return b;
if (t == d) return b + c;
if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
}
// 编写animate类
class Animate {
constructor(dom) {
this.dom = dom;//dom元素
this.startTime = 0;// 开始时间
this.duration = null;//持续时间
this.startPos = 0;//开始位置
this.endPos = 0;//结束位置
this.propertyName = null;//dom样式,要改变的样式
this.easing = null;// 缓动算法
}
start(propertyName, endPos, duration, easing) {
this.startPos = this.dom.getBoundingClientRect()[propertyName]
this.startTime = new Date().getTime();
this.propertyName = propertyName;
this.endPos = endPos;
this.duration = duration;
this.easing = tween[easing];
const timeId = setInterval(() => {
if (this.step() === false) {
clearInterval(timeId);
}
}, 19)
}
//表示小球每一帧要做的事情
step() {
const t = new Date().getTime();
if (t >= this.startTime + this.duration) {
// 修正位置
this.update(this.endPos);
return false
}
const pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
this.update(pos)
}
update(pos) {
this.dom.style[this.propertyName] = pos + 'px'
}
}
// const div1 = document.querySelector('.box1');
// const animate = new Animate(div1);
// animate.start('left', 300, 2000, 'expoEaseOut');
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box1 {
position: absolute;
background: blue;
}
</style>
</head>
<body>
<div class="box1">我是div</div>
<script src="./3-策略模式实现动画.js"></script>
</body>
</html>
表单验证
在以前我写的一个项目中有一个页面的表单有七八条,每一条都需要验证并提示对应信息并没有填写,当时的我就是写了大量的if语句来进行判断,现在想想使用策略模式实现是真的优雅。
以下是一个较完整的表单验证:
const strategies = {
isNoEmpty(value, errMsg) {
if (value === '') {
return errMsg
}
},
minLength(value, min, errMsg) {
if (typeof value === 'string') {
if (value.length < min) {
return errMsg
}
}
else if (typeof value === 'number') {
if (value < min) {
return errMsg
}
}
},
isMobile(value, errMsg) {
if (!/^1[3-9]\d{9}$/.test(value)) {
return errMsg;
}
}
}
// 实现validator类
class Validator {
// 保存校验规则
constructor() {
this.cache = [];
}
add(dom, rules) {
for (let i = 0, rule; rule = rules[i++];) {
const strategyAry = rule.strategy.split(':');
const errorMsg = rule.errMsg;
this.cache.push(function () {
console.log(strategyAry);
const 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++];) {
const errMsg = validatorFunc();
if (errMsg) {
return errMsg
}
}
}
}
const form = document.getElementById('registerForm');
function validatorFunc() {
const validator = new Validator();
validator.add(form.userName, [{
strategy: 'isNoEmpty',
errMsg: '用户名不能为空'
}, {
strategy: 'minLength:8',
errMsg: '不能小于8个数'
}]);
validator.add(form.password, [{
strategy: 'isNoEmpty',
errMsg: '密码不能为空'
}, {
strategy: 'minLength:8',
errMsg: '不能小于8个数'
}]);
validator.add(form.phoneNumber, [{
strategy: 'isNoEmpty',
errMsg: '手机号不能为空'
}, {
strategy: 'isMobile',
errMsg: '格式不正确'
}]);
return validator.start()
}
form.onsubmit = function () {
const errMsg = validatorFunc();
if (errMsg) {
alert(errMsg)
return false;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="#" id="registerForm">
<input type="text" name="userName">
<input type="password" autocomplete name="password">
<input type="text" name="phoneNumber">
<button>提交</button>
</form>
<!-- <script src="./4-表单验证.js"></script> -->
<script src="./5-表单验证改.js"></script>
</body>
</html>
当然可以在进一步修改下:
//策略对象
const strategies = {
errorMsg(message, field = "filed") {
return message ?? `The ${field} not follow the rule: ${this.errorMsg.caller.name}`
},
required(value, { message, required: isEmpty }, filedname) {
if (isEmpty && value === '') {
return this.errorMsg(message, filedname)
}
},
max(value, { message, max: length, type }, filedname) {
if (type === "string" && value.length > length) {
return this.errorMsg(message, filedname);
}
if (type === "number" && Number(value) > length) {
return this.errorMsg(message, filedname);
}
},
min(value, { message, min: length, type = "string" }, filedname) {
if (type === "string" && value.length < length) {
return this.errorMsg(message, filedname);
}
if (type === "number" && Number(value) < length) {
return this.errorMsg(message, filedname);
}
},
parttern(value, { message, parttern: reg }, filedname) {
if (!reg.test(value)) {
return this.errorMsg(message, filedname);
}
}
}
// 实现validator类
class Validator {
// 保存校验规则
constructor(dom) {
this.dom = dom
this.cache = [];
}
add(filedname, rules) {
for (let i = 0, rule; rule = rules[i++];) {
const options = rule;
// 移除message/type等不是策略像项的key
const keys = Object.keys(options).filter(key => (key !== 'message' && key !== 'type'));
// 注意这里的策略像如果一个options写了多个只会对第一个生效
this.cache.push(() => {
if (!strategies?.[keys[0]]) {
throw new Error('请填写校验规则');
}
return strategies[keys[0]](this.dom?.[filedname]?.value, options, filedname)
})
}
}
start() {
for (let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
const errMsg = validatorFunc();
console.log(errMsg);
if (errMsg) {
return errMsg
}
}
}
}
const form = document.getElementById('registerForm');
function validatorFunc() {
const validator = new Validator(form);
validator.add('userName', [{
required: true,
message: '用户名不能为空'
}, {
min: 8,
message: '不能小于8个数'
}]);
validator.add('password', [{
required: true,
message: '密码不能为空'
}, {
min: 8,
message: '密码不能小于8个数'
}]);
validator.add('phoneNumber', [{
required: true,
message: '手机号不能为空'
}, {
parttern: /^1[3-9]\d{9}$/,
message: '格式不正确'
}]);
return validator.start()
}
form.onsubmit = function () {
const errMsg = validatorFunc();
if (errMsg) {
alert(errMsg)
return false;
}
}
总结
通过这三个例子的演示我们可以看出策略模式有以下优点:
- 策略模式利用委托,多态,组合等技术,有效的避免多重条件判断。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategies中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
当然该模式也有一定的缺点:
- 使用策略模式会在程序中增加许多策略类或者策略对象,这些算法不一定都会用到。
- 用户必须了解所有的strategy,比如你出行会先选择很多交通工具,你必须要去了解所有交通工具的细节才能选择,这是违反最少知识原则的。
当然在JavaScript中策略类往往是被函数代替的,这样也更加的灵活适用。
代理模式
定义:当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
虚拟代理
代理其实还分保护代理和虚拟代理,保护代理用于控制不同权限的对象对目标对象的访问。但是在JavaScript中实现保护代理不是那么的容易,因为我们不知道是谁访问了对象。所以接下来讨论的都是虚拟代理。
以下展示一个虚拟代理实现图片预加载:
const CreateImage = (
function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc(src) {
imgNode.width = 200
imgNode.src = src;
imgNode.alt = src
}
}
}()
);
const imgUrl = 'https://avatars.githubusercontent.com/u/5378891?s=48&v=4'
//CreateImage.setSrc('https://topzhang.cn/usr/assets/img/saber-header.jpg')
// 引入代理对象
const proxyImage = (function () {
// 这里先创建一个img实例来异步加载图片,然后监听这个图片加载完毕后再将实际要展示图片的img容器地址替换为加载完的url,此时就直接从内存读取加载完的图片。
const img = new Image();
// 监听是否加载完
img.onload = function () {
CreateImage.setSrc(this.src)
}
return {
setSrc(src) {
CreateImage.setSrc('./image.png');
// 保存要加载的图片
img.src = src
}
}
}())
proxyImage.setSrc(imgUrl)
上面这段代码其实不使用代理也可以实现,但是我们尽量要求一个独享符合单一职责原则,不要吧过多的功能都揉合在一个对象上。而使用代理就可以将这两个功能粉离开,如果以后我们不需要预加载了修改起来也很方便,同时使用代理这里也遵循了接口一致,在使用上也很透明,不会增加用户的使用成本。
虚拟代理实现接口合并
有很多的checkbox,点击一下checkbox就会发送一次网络请求,当用户频繁的点击就会频繁的发送网络请求,这对于性能的开销是非常大的。所以对于频繁的点击发送请求,我们可以先记录点击的checkbox,然后延迟一段时间后合并结果发出请求。其实就是做个防抖处理,就能极大的降低服务器的压力了。
const synchronousFile = function (id) {
console.log('开始同步', id, '文件')
}
const checkboxs = document.getElementsByTagName('input');
for (let i = 0, c; c = checkboxs[i++];) {
c.onclick = function () {
if (this.checked = true) {
//synchronousFile(this.id)
proxySynchronousFile
(this.id)
}
}
}
// 这里我们点击一次checkbox就会同步一次,如果用户频繁的点击这会有性能瓶颈的
const proxySynchronousFile = (function () {
// 缓存
const cache = [];
let timer;
return function (id) {
cache.push(id);
if (timer) {
return;
}
timer = setTimeout(() => {
synchronousFile(cache.join(','));
clearTimeout(timer);
timer = null;
//清空数组
cache.length = 0
}, 2000)
}
}());
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="checkbox" id="1">
<input type="checkbox" id="2">
<input type="checkbox" id="3">
<input type="checkbox" id="4">
<input type="checkbox" id="5">
<input type="checkbox" id="6">
<input type="checkbox" id="7">
<input type="checkbox" id="8">
<input type="checkbox" id="9">
<input type="checkbox" id="10">
<script src="./2-虚拟代理合并http请求.js"></script>
</body>
</html>
虚拟代理实现惰性加载
一个mini的打印控制台miniConsole.js。用户可以使用其log方法打印变量,然后按下f2就会在页面创建一个窗口显示打印信息。当然用户可能不会使用miniConsole,也可能不会按下f2打印结果,所以我们并不需要一开始就加载该js文件,我们可以在用户按下f2后在加载该文件,当然在那之前如果用户要使用log方法我们就需要先提供一个代理miniConsole对象提供代理的log方法保存用户实际要执行的log方法。
// 这里是代理miniConsole 在用户还没按下f2前用于保存要执行的log函数,等到按下后新加载的log函数就会代替这个函数然后执行内部保存的函数
let miniConsole = (function () {
const cache = []
function handler(ev) {
console.log(cache);
if (ev.keyCode === 113) {
const script = document.createElement('script');
// 加载完真正的miniconsole然后执行
script.onload = function () {
for (let i = 0, fn; fn = cache[i++];) {
fn();
}
};
script.src = './miniConsole.js';
document.getElementsByTagName('head')[0].appendChild(script);
document.body.removeEventListener('keydown', handler, false)
}
}
document.body.addEventListener('keydown', handler, false);
return {
log(...args) {
cache.push(function () {
return miniConsole.log.apply(miniConsole, args)
})
}
}
}());
miniConsole.log(22)
miniConsole.log(33)
缓存代理
function mult() {
console.log('开始计算乘积');
let a = 1;
for (let i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
}
//添加缓存代理函数(当然该函数还可以讲话公共分布提取出来设置成一个通用的代理缓存工厂函数)
const proxyMult = (function () {
const cache = {};
return function (...args) {
const multArgs = args.join(',');
if (multArgs in cache) {
return cache[multArgs]
}
// 缓存
return cache[multArgs] = mult.apply(this, args);
}
}())
console.log(proxyMult(2, 3));
console.log(proxyMult(2, 3));
总结
上述案例演示了代理的基本用法,实际上代理的使用方式有很多:
- 防火墙代理 ----- 控制网络资源的访问
- 远程代理 ----- 为一个对象在不同的地址空间提供局部代表
- 保护代理 ----- 用于对象有不同访问权限的情况
- 智能引用代理 ----- 取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
- 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。
代理模式可以让逻辑更加简洁清晰,使得对象的功能不再耦合。但是要注意我们在编写代码的时候实现不用可以的去使用代理模式,等到确实不方便访问某个对象的时候在写代理也不迟。
迭代器模式
定义:提供一个方顺序的访问目标对象中的各个元素,而又不需要暴露该对象的内部实现。
目前大多数语言都内置了迭代器。
实现一个简单的迭代器
const each = function (ary, callback) {
for (let i = 0, l = ary.length; i < l; i++) {
callback.call(ary[i], ary[i], i)
}
}
each([1, 2, 3, 4, 5], function (item, index) {
console.log(item, index);
})
内部迭代器和外部迭代器
迭代器可以分为内部迭代器和外部迭代器,在刚刚我们编写的each就是一个内部迭代器,外部只需要一次初始化调用即可。
外部迭代器必须显式的请求迭代下一个元素,当然也增加了调用的复杂度,但是我们可以很方便的控制迭代过程。
以下展示一个比较两个数组是否相等的案例:
// 外部迭代
class Iterator {
constructor(obj) {
this.obj = obj;
this.current = 0;
}
next() {
this.current++;
}
done() {
return this.current >= this.obj.length
}
getCurrentItem() {
return this.obj[this.current]
}
}
const i1 = new Iterator([1, 2, 3]);
const i2 = new Iterator([1, 2, 3]);
function compare(i1, i2) {
if (i1.length !== i2.length) {
return false;
}
while (!i1.done() && !i2.done()) {
if (i1.getCurrentItem() !== i2.getCurrentItem()) {
return false;
}
i1.next();
i2.next();
}
return true;
}
console.log(compare(i1, i2));
倒序迭代器
分分钟实现:
const each = function (ary, callback) {
for (let i = ary.length; i >= 0; i--) {
callback.call(ary[i], ary[i], i)
}
}
each([1, 2, 3, 4, 5], function (item, index) {
console.log(item, index);
});
终止迭代器
const each = function (ary, callback) {
for (let i = 0, l = ary.length; i < l; i++) {
const res = callback.call(ary[i], ary[i], i)
if (res === false) {
break;
}
}
}
each([1, 2, 3, 4, 5], function (item, index) {
console.log(item);
if (item === 3) {
return false
}
});
总结
迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器。
发布订阅模式
定义:发布订阅模式又称之为观察者模式,定义对象之间的一对多的依赖关系,当一个对象的状态发生改变的时候所有依赖它的对象都会得到通知。
当然发布订阅也是设计模式中最常见的一种模式了,其作用非常强大,现如今的前端框架,比如React/Vue
中都能看到该模式,另外我在手写Promise
源码的时候也用到了发布订阅模式,遇到复杂的跨组件传值情况也可以使用发布订阅来进行传递。
总之该模式实用性很强,是个必须掌握的一个知识点。
基本的发布订阅器
首先实现一个基本的发布订阅器,功能具备:
- 事件的监听,可以为一个事件类型添加多个函数进行监听。
- 事件的触发,触发事件会执行所有该事件的监听函数。
- 事件的移除,可以移除同一个事件的某一个监听函数,也可以移除某一个事件的所有监听函数。
const events = {
/*
保存事件的一个对象,基本结构为{key1:fn[],key2:fn[],...}
你也可以使用map代替普通的对象
*/
eventList: {},
listen(key, fn) {
if (!this.eventList[key]) {
this.eventList[key] = []
}
this.eventList[key].push(fn);
},
trigger(key, ...args) {
fns = this.eventList[key];
if (!fns || fns.length === 0) {
return false;
}
for (let i = 0, fn; fn = fns[i++];) {
fn.apply(this, args)
}
}
,
remove(key, fn) {
const fns = this.eventList[key];
// 如果用户没有传一个指定函数就删除该类型下的所有函数
if (!fns) {
return false
}
if (!fn) {
fns && (fns.length = 0)
} else {
for (let l = fns.length - 1; l >= 0; l++) {
const _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
}
}
}
测试用例:
events.listen('home', function (price, square) {
console.log('价格', price);
console.log('面积', square);
})
events.listen('home', function (price, square) {
console.log('面积', square);
})
events.listen('home', function (price) {
console.log('价格', price);
})
events.listen('car', function (price) {
console.log('car价格', price);
})
events.remove('car')
events.trigger('home', 2000, 88)
events.trigger('car', 20010, 881)
// 价格 2000
// 面积 88
// 面积 88
// 价格 2000
上述代码实现了一个基本的发布订阅功能,接下来我们实现一个功能较为丰富的发布订阅器。
进阶的发布订阅器
功能具备:
- 上述基本的所有功能。
- 支持单次监听,即该类型事件只会被监听一次。
- 唯一订阅,同类型下,唯一订阅前的所有非唯一订阅都会失效。
- 支持先发布后订阅。
- 支持类型订阅数量限制。
- 具有命名空间。
说明:
先发布后订阅功能,我们上面实现的基本发布订阅器,是需要执行订阅器,然后执行发布器触发,如果先发布的话,那是后续的订阅器是不会执行的了。而我们需要实现先执行发布类型事件(可以发布多个),然后后续遇到订阅了相同事件的订阅器就会执行先发布的类型事件,这里我们的逻辑就是只能触发一次先发布的类型事件。可以理解为离线消息只能被已读一次。
命名空间则是考虑到大量使用发布订阅的情况,遇到了多个事件想使用同一个类型,所以可以加上一个命名空间来进行区分。
class MyEvent {
constructor() {
// 命名空间
this.namespaceCache = {};
}
// 默认的命名空间
static _default = "default"
//遍历执行函数的迭代器
static each(arr, fn) {
let ret;
for (let i = 0, l = arr.length; i < l; i++) {
const itemFn = arr[i];
ret = fn(itemFn, i)
}
return ret;
}
// 默认最大监听函数数量
static defaultMaxListeners = 5
// 设置最大监听函数数量
setMaxListeners(count) {
this._count = count
}
// 获取最大监听函数数量
getMaxListeners() {
return this._count ? this._count : MyEvent.defaultMaxListeners
}
// 基础的监听函数
_on(key, fn, cache) {
let fns = cache.get(key);
if (!fns) {
fns = [];
}
if (fns.length >= this.getMaxListeners()) {
return
}
fns.push(fn);
cache.set(key, fns);
}
_remove(key, fn, cache) {
let fns = cache.get(key);
//删除具体的函数
if (fn) {
for (let i = fns.length; i >= 0; i--) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
// 没有传递就删除所有函数
} else {
fns && (fns.length = 0)
}
}
_emit(key, cache, ...args) {
const fns = cache.get(key);
if (!fns || !fns.length) {
return;
}
// 这里用箭头函数的话内部绑定的this就是无效的,另外这里封装一个迭代器是为了接受返回值
return MyEvent.each(fns, (itemFn) => {
return itemFn.apply(this, args);
});
}
// 创建命名空间
create(namespace = MyEvent._default) {
const cache = new Map();
// 保存先发布的的类型和参数数组,这是支持先发布后订阅功能关键
let offlineStack = [];
const childEvent = {
on: (key, fn, isLast = false) => {
this._on(key, fn, cache);
// 说明没有已经发布的了
if (offlineStack === null) {
return
}
// isLast 这个参数进行先发布后订阅生效,主要目的是后续的订阅只会对最后一个发布生效
if (isLast) {
offlineStack.length && offlineStack.pop()();
} else {
MyEvent.each(offlineStack, (itemFn) => {
itemFn()
})
}
// 订阅的时候只能执行一次预先的发布函数,后续的订阅就不执行了
offlineStack = null;
},
// 订阅的时候在这之前的所有订阅都会失效
one: (key, fn, isLast = false) => {
// 清除所有保存的订阅函数,只执行当前的
this._remove(key, null, cache);
childEvent.on(key, fn, isLast)
},
once: (key, fn, isLast) => {
const wrap = (...args) => {
fn.apply(this, args);
childEvent.remove(key, wrap)
}
childEvent.on(key, wrap, isLast)
},
remove: (key, fn) => {
this._remove(key, fn, cache);
},
emit: (key, ...args) => {
// 将参数保存
const fn = () => {
return this._emit.call(this, key, cache, ...args)
}
// 如果有说明存在还没被订阅就先发布了,所以保存在栈中
if (offlineStack) {
return offlineStack.push(fn);
}
// 否则就是正常订阅发布顺序,直接执行
return fn();
}
}
// 保存命名空间
return this.namespaceCache[namespace] ? this.namespaceCache[namespace] : this.namespaceCache[namespace] = childEvent
}
// 考虑没有设置命名空间的情况,返回代理方法
on(key, fn, isLast) {
const event = this.create();
event.on(key, fn, isLast)
}
one(key, fn, isLast) {
const event = this.create();
event.one(key, fn, isLast)
}
once(key, fn, isLast) {
const event = this.create();
event.once(key, fn, isLast)
}
emit(key, ...args) {
const event = this.create();
event.emit(key, ...args)
}
remove(key, fn) {
const event = this.create();
event.remove(key, fn)
}
}
module.exports = new MyEvent();
测试用例:
// 这里默认最大监听数是 2
Event.on('car', (price) => {
console.log('汽车降价了', price)
})
Event.on('car', (price) => {
console.log('汽车又降价了', price - 1000)
})
Event.on('car', (price) => {
console.log('汽车内购价', price - 10000)
})
Event.emit('car', 50000);
// 汽车降价了 50000
// 汽车又降价了 49000
Event.setMaxListeners(5)
Event.on('car', (price) => {
console.log('汽车降价了', price)
})
Event.on('car', (price) => {
console.log('汽车又降价了', price - 1000)
})
Event.on('car', (price) => {
console.log('汽车内购价', price - 10000)
})
Event.emit('car', 50000)
// 汽车降价了 50000
// 汽车又降价了 49000
// 汽车内购价 40000
// 测试先发布订阅
Event.emit('car', 50000)
Event.on('car', (price) => {
console.log('汽车降价了', price)
})
// 后续的订阅不会触发
Event.on('car', (price) => {
console.log('汽车又降价了', price - 1000)
})
// 汽车降价了 50000
// 多个发布
Event.emit('car', 50000)
Event.emit('car', 50001)
Event.emit('car', 50002)
Event.on('car', (price) => {
console.log('汽车降价了', price)
})
// 后续的订阅不会触发
Event.on('car', (price) => {
console.log('汽车又降价了', price - 1000)
})
// 汽车降价了 50000
// 汽车降价了 50001
// 汽车降价了 50002
// 测试isLast
Event.emit('car', 50000)
Event.emit('car', 50001)
Event.emit('car', 50002)
Event.on('car', (price) => {
console.log('汽车降价了', price)
}, true)
// 后续的订阅不会触发
Event.on('car', (price) => {
console.log('汽车又降价了', price - 1000)
})
// 汽车降价了 50002
// 测试once
Event.on('car', (price) => {
console.log('常年促销', price)
})
Event.once('car', (price) => {
console.log('汽车降价了', price)
})
Event.emit('car', 50000)
Event.emit('car', 50000)
// 常年促销 50000
// 汽车降价了 50000
// 常年促销 50000
// 测试唯一订阅,同类型下,唯一订阅前的所有非唯一订阅都会失效(同一命名空间下)
Event.setMaxListeners(5)
Event.on('car', (price) => {
console.log('常年促销1', price)
})
Event.on('car', (price) => {
console.log('常年促销2', price)
})
Event.on('car', (price) => {
console.log('常年促销3', price)
})
Event.one('car', (price) => {
console.log('只有我能订阅', price)
})
Event.emit('car', 50000)
Event.emit('car', 50000)
// 只有我能订阅 50000
// 只有我能订阅 50000
// 测试命名空间
Event.setMaxListeners(5)
Event.create('namespace1').on('car', (price) => {
console.log('常年促销1', price);
})
Event.create('namespace2').on('car', (price) => {
console.log('常年促销2', price);
})
Event.one('car', (price) => {
console.log('只有我能订阅', price);
})
Event.emit('car', 50000);
Event.create('namespace1').emit('car', 50000);
Event.create('namespace2').emit('car', 50000);
// 只有我能订阅 50000
// 常年促销1 50000
// 常年促销2 50000
总结
上述Javascript
实现的的发布订阅模式其实还是和一些语言有一些不同。一些其他的语言相较JavaScript
可能实现较为复杂,JavaScript
可以将函数作为参数的特性,使得实现起来更加优雅和简单。
发布订阅模式优点很明显,对于对象二者通信的结耦,在异步编程中也可以实现更加简洁松耦合的代码编写。
但是该模式也不可避免的有一些缺点,毕竟发布订阅模式内部还是有闭包的存在,导致订阅者会一直存在内存中。另外我们过度的使用发布订阅会导致对象与对象之间的关系变得不可捉摸,同时多个发布订阅嵌套在一起导致的bug
可能更加不可排查。
一句话,合理利用,不可滥用。
命令模式
定义:将请求封装成一个对象,从而可以使用不同的请求让客户端参数化,对请求排队或者记录请求日志,可以提供请求的撤销和恢复。
传统的命令模式
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const button3 = document.getElementById('button3');
function setCommand(button, command) {
button.onclick = function () {
command.execute()
}
}
const MenuBar = {
refresh() {
console.log('刷新菜单')
}
}
const SubMenu = {
add() {
console.log('增加子菜单')
},
del() {
console.log('删除子菜单')
}
}
class RefreshBarCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
class AddSubMenuCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.add()
}
}
class DelSubMenuCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.del()
}
}
const refreshMenuBarCommand = new RefreshBarCommand(MenuBar);
const addSubMenuCommmand = new AddSubMenuCommand(SubMenu);
const delSubMenuCommmand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommmand);
setCommand(button3, delSubMenuCommmand);
将命令封装为一个对象然后提供一个公共的接口供执行命令的方法调用。这样就分离的命令的定义和执行的逻辑
JavaScript命令模式
// 可以不需要专门定义一个只执行execute的类,利用闭包也可以直接实现
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const button3 = document.getElementById('button3');
function setCommand(button, func) {
button.onclick = function () {
func()
}
}
const MenuBar = {
refresh() {
console.log('刷新菜单')
}
}
const SubMenu = {
add() {
console.log('增加子菜单')
},
del() {
console.log('删除子菜单')
}
}
// class RefreshBarCommand {
// constructor(receiver) {
// this.receiver = receiver;
// }
// execute() {
// this.receiver.refresh();
// }
// }
// class AddSubMenuCommand {
// constructor(receiver) {
// this.receiver = receiver
// }
// execute() {
// this.receiver.add()
// }
// }
// class DelSubMenuCommand {
// constructor(receiver) {
// this.receiver = receiver
// }
// execute() {
// this.receiver.del()
// }
// }
// const refreshMenuBarCommand = new RefreshBarCommand(MenuBar);
// const addSubMenuCommmand = new AddSubMenuCommand(SubMenu);
// const delSubMenuCommmand = new DelSubMenuCommand(SubMenu);
setCommand(button1, MenuBar.refresh);
setCommand(button2, SubMenu.add);
setCommand(button3, SubMenu.del);
可以看出灵活的JavaScript实现命令模式是十分简单的,当然如果你想更加规范的话,可以讲所有方法使用execute包裹一下,统一调用的接口。
// 当然以execute来执行还是更能展现我们使用的是命令模式
// 可以不需要专门定义一个只执行execute的类,利用闭包也可以直接实现
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const button3 = document.getElementById('button3');
function setCommand(button, command) {
button.onclick = function () {
command.execute();
}
}
function RefreshBarCommand(receiver) {
return {
execute() {
receiver.refresh();
}
}
}
const MenuBar = {
refresh() {
console.log('刷新菜单')
}
}
const SubMenu = {
add() {
console.log('增加子菜单')
},
del() {
console.log('删除子菜单')
}
}
const refreshMenuBarCommand = RefreshBarCommand(MenuBar)
setCommand(button1, refreshMenuBarCommand);
撤销命令
这里利用到了策略模式使用的动画代码,用于演示开始动画并撤销动画的过程:
// Animate.js
/*
* t: current time(当前时间);当前时刻-开始时刻
* b: beginning value(初始值);初始位置
* c: change in value(变化量);目标位置-变化量
* d: duration(持续时间)。
*/
const tween = {
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
easeOut: function (t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
},
easeInOut: function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
},
expoEaseIn: function (t, b, c, d) {
return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
},
expoEaseOut: function (t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
},
expoEaseInOut: function (t, b, c, d) {
if (t == 0) return b;
if (t == d) return b + c;
if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
}
// 编写animate类
class Animate {
constructor(dom) {
this.dom = dom;//dom元素
this.startTime = 0;// 开始时间
this.duration = null;//持续时间
this.startPos = 0;//开始位置
this.endPos = 0;//结束位置
this.propertyName = null;//dom样式,要改变的样式
this.easing = null;// 缓动算法
}
start(propertyName, endPos, duration, easing) {
this.startPos = this.dom.getBoundingClientRect()[propertyName]
this.startTime = new Date().getTime();
this.propertyName = propertyName;
this.endPos = endPos;
this.duration = duration;
this.easing = tween[easing];
const timeId = setInterval(() => {
if (this.step() === false) {
clearInterval(timeId);
}
}, 19)
}
//表示小球每一帧要做的事情
step() {
const t = new Date().getTime();
if (t >= this.startTime + this.duration) {
// 修正位置
this.update(this.endPos);
return false
}
const pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
this.update(pos)
}
update(pos) {
this.dom.style[this.propertyName] = pos + 'px'
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>带撤销的命令模式</title>
<style>
#ball {
top: 60px;
position: absolute;
background-color: #000;
width: 50px;
height: 50px;
}
</style>
<script src="../5-策略模式/3-策略模式实现动画.js"></script>
</head>
<body>
<div id="ball"></div>
请输入小球移动距离:<input
id="pos"
placeholder="输入小球移动距离"
type="text"
/>
<button id="moveBtn">开始移动</button>
<button id="cancelBtn">取消移动</button>
</body>
<script>
const ball = document.getElementById("ball");
const pos = document.getElementById("pos");
const moveBtn = document.getElementById("moveBtn");
const cancelBtn = document.getElementById("cancelBtn");
// moveBtn.onclick = function () {
// const animate = new Animate(ball);
// animate.start('left', pos.value, 1000, 'expoEaseIn')
// }
class MoveCommand {
constructor(receiver, pos) {
this.receiver = receiver;
this.pos = pos;
this.oldPos = null;
}
execute() {
this.receiver.start("left", this.pos, 1000, "expoEaseIn");
// 记录小球开始的位置
this.oldPos =
this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
}
undo() {
this.receiver.start("left", this.oldPos, 1000, "expoEaseIn");
}
}
let moveCommand;
moveBtn.onclick = function () {
const animate = new Animate(ball);
moveCommand = new MoveCommand(animate, +pos.value);
moveCommand.execute();
};
cancelBtn.onclick = function () {
moveCommand.undo();
};
</script>
</html>
用户点击开始按钮的时候会记录小球的开始位置并开始移动到指定位置,之后撤点击取消按钮就会返回原来的起点。
宏命令
宏命令是一组命令的集合。我们将一组命令添加到命令执行对象中,然后遍历命令执行共同的接口(execute)
const closeDoorCommand = {
execute() {
console.log('关门')
}
}
const openPcCommand = {
execute() {
console.log('关电脑')
}
}
const openQQCommand = {
execute() {
console.log('登陆qq')
}
}
class MacroCommand {
constructor() {
this.commandsList = []
}
add(command) {
this.commandsList.push(command)
}
execute() {
for (const command of this.commandsList) {
command.execute();
}
}
}
const macroCommand = new MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute()
小结
可以看到在JavaScript中命令模式实现起来更加简洁,可以利用高阶函数实现该模式。命令模式在JavaScript中也算事一种隐形的模式。
组合模式
定义:用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。
回顾之前定义的宏命令,我们可以看出,整个宏命令和子命令是成树形结构的。相比子命令,宏命令就相当于是子命令的代理。
另外组合模式还可以利用多态来实现递归执行,比如子命令又是一个很多子命令的集合。我们只需要调用公共的执行接口(execute)就能传播执行所有命令。
一个更强大的宏命令:
class MacroCommand {
constructor() {
this.commandsList = []
}
add(command) {
this.commandsList.push(command)
}
execute() {
for (const command of this.commandsList) {
command.execute();
}
}
}
const openAcCommand = {
execute() {
console.log('打开空调')
}
}
const openTvCommand = {
execute() {
console.log('打开电视')
}
}
const openSoundCommand = {
execute() {
console.log('打开音响')
}
}
const macroCommand1 = new MacroCommand();
macroCommand1.add(openSoundCommand);
macroCommand1.add(openTvCommand)
const closeDoorCommand = {
execute() {
console.log('关门')
}
}
const openPcCommand = {
execute() {
console.log('关电脑')
}
}
const openQQCommand = {
execute() {
console.log('登陆qq')
}
}
const macroCommand2 = new MacroCommand();
macroCommand2.add(closeDoorCommand);
macroCommand2.add(openPcCommand);
macroCommand2.add(openQQCommand);
// 组装一个超级宏命令
const macroCommand = new MacroCommand();
macroCommand.add(openAcCommand);
macroCommand.add(macroCommand1);
macroCommand.add(macroCommand2);
const button = document.getElementById('button')
const setCommand = function (dom, command) {
dom.onclick = function () {
command.execute();
}
}
setCommand(button, macroCommand)
当然我们要注意,执行子命令的父命令往往都有add方法,用于添加子命令,但是作为叶子结点的子命令是没有添加命令的方法的,所以为了避免用户误操作,保证透明性,我们可以可以在子命令中添加add方法,内部抛出异常即可。
const xxxCommand = {
execute() {
console.log('打开空调')
}
add(){
throw new Error('子命令不能添加命令')
}
}
扫描文件夹
文件和文件夹之间的关系非常适合用组合模式来进行模拟,我们访问扫描文件的时候并不需要关心其内部有多少文件或文件夹,只需要从最外层文件扫描,自动进行递归扫描所有文件。
class Folder {
constructor(name) {
this.name = name;
this.files = [];
}
add(file) {
this.files.push(file)
}
scan() {
console.log('开始扫描文件夹---' + this.name)
for (const file of this.files) {
file.scan();
}
}
}
class File {
constructor(name) {
this.name = name
}
add() {
throw new Error('文件下面不能添加文件')
}
scan() {
console.log('开始扫描文件---' + this.name)
}
}
const folder = new Folder('学习资料');
const folder1 = new Folder('JavaScript')
const folder2 = new Folder('JQuery')
const file1 = new File('JavaScript设计模式与开发实践')
const file2 = new File('精通JQuery')
const file3 = new File('重构与模式');
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
const folder3 = new Folder('NodeJs');
const file4 = new File('深入浅出Node.js');
folder3.add(file4)
const file5 = new File('JavaScript语言精髓与编程实践');
folder.add(folder3);
folder.add(file5);
folder.scan()
可以看到我们只需执行最顶端的scan方法就可以扫描其下所有的文件/文件夹了。
另外注意组合模式子命令和父命令这种树形结构关系并不是父子关系,(为了方便我们才这样称呼)组合模式是一种HAS-A(聚合)的关系,而不是IS-A。组合对象包含一组叶对象,但Leaf并不是Composite的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口。
引用父对象
有时候我们需要在子节点上保持对父节点的引用,比如在组合模式中使用职责链时,有可能需要让请求从子节点往父节点上冒泡传递。还有当我们删除某个文件的时候,实际上是从这个文件所在的上层文件夹中删除该文件的。
class Folder {
constructor(name) {
this.name = name;
this.files = [];
this.parent = null;
}
add(file) {
// 设置父对象
file.parent = this;
this.files.push(file)
}
scan() {
console.log('开始扫描文件夹---' + this.name)
for (const file of this.files) {
file.scan();
}
}
remove() {
if (!this.parent) {
// 根结点或者还没添加的游离节点
return
}
for (let files = this.parent.files, l = files.length - 1; l >= 0; l--) {
const file = files[l];
//从父文件中找到自己,然后滚蛋
if (file === this) {
console.log('移除文件夹---' + this.name);
files.splice(l, 1)
}
}
}
}
class File {
constructor(name) {
this.name = name
this.parent = null;
}
add() {
throw new Error('文件下面不能添加文件')
}
scan() {
console.log('开始扫描文件---' + this.name)
}
remove() {
if (!this.parent) {
// 根结点或者还没添加的游离节点
return
}
for (let files = this.parent.files, l = files.length - 1; l >= 0; l--) {
const file = files[l];
//从父文件中找到自己,然后滚蛋
if (file === this) {
console.log('移除文件---' + this.name)
files.splice(l, 1)
}
}
}
}
const folder = new Folder('学习资料');
const folder1 = new Folder('JavaScript')
const folder2 = new Folder('JQuery')
const file1 = new File('JavaScript设计模式与开发实践')
const file2 = new File('精通JQuery')
const file3 = new File('重构与模式');
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
const folder3 = new Folder('NodeJs');
const file4 = new File('深入浅出Node.js');
folder3.add(file4)
const file5 = new File('JavaScript语言精髓与编程实践');
folder.add(folder3);
folder.add(file5);
folder.scan()
// 移除文件/夹
folder1.remove()
file5.remove()
folder.scan()
总结
组合模式适用于以下两种情况:
- 表示对象的部分-整体层次结构。
- 客户希望统一对待树中的所有对象。
组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。
当然也有一定的缺点,大量的使用组模式会导致创建出来的对象之前区别不是很大,其区别只有在运行的时候才会显示出来,这会使代码难以理解。另外过多的使用组合模式创建大量的对象也会对代码性能瓶颈造成影响。
模版方法模式
含义:平行的各个子类之间有一些相同的行为,也有一些不同的行为。该模式就是将各个子类相同的行为单独提取出来进行复用。
模版方法模式其实就是只需继承就可以实现的一种简单的模式。模版方法模式一般有两个部分组成,一部分是抽象类,第二部分是具体的实现子类。
泡饮料
class Beverage {
boilWater() {
console.log('把水煮沸')
}
brew() {
// 子类重写
}
pourInCup() {
//空方法
}
addCondiments() {
// 空方法
}
// 模版方法
init() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
}
class Coffee extends Beverage {
brew() {
console.log('沸水泡咖啡')
}
pourInCup() {
console.log('把咖啡倒进杯子')
}
addCondiments() {
console.log('加牛奶')
}
}
const coffee = new Coffee();
coffee.init();
class Tea extends Beverage {
brew() {
console.log('沸水浸泡咖啡')
}
pourInCup() {
console.log('把茶叶倒进杯子')
}
addCondiments() {
console.log('加柠檬')
}
}
const tea = new Tea();
//tea.init();
上述的模版方法就是init,内部执行函数顺序一样,只是不同的子类对函数有不同的实现。
抽象类
由于JavaScript没有抽象类的概念(你可以使用TypeScript),所以我们在使用模版方法可能会遇到一些问题。
比如:子类忘记实现父类方法。
当然这里有几种解决方法:
- 使用鸭子类型来检测子类是否有实现方法,但是这实现起来比较的复杂而且也加入 了很多与业务无关的代码
- 在父类方法中抛出异常,如果子类没重写那就会报错。
当然第二种方式是最简单的,但用户要得到消息需要等到执行代码的时候。
钩子方法
class Beverage {
boilWater() {
console.log('把水煮沸')
}
brew() {
// 子类重写
}
pourInCup() {
//空方法
}
addCondiments() {
// 空方法
}
customerWantsComdiments() {
// 默认需要
return true;
}
// 模版方法
init() {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsComdiments()) {
this.addCondiments();
}
}
}
class Coffee extends Beverage {
brew() {
console.log('沸水泡咖啡')
}
pourInCup() {
console.log('把咖啡倒进杯子')
}
customerWantsComdiments() {
// 默认需要
return window.confirm('需要调料吗');
}
addCondiments() {
console.log('加牛奶')
}
}
const coffee = new Coffee();
coffee.init();
class Tea extends Beverage {
brew() {
console.log('沸水浸泡咖啡')
}
pourInCup() {
console.log('把茶叶倒进杯子')
}
addCondiments() {
console.log('加柠檬')
}
}
const tea = new Tea();
tea.init();
在父类添加一个钩子方法用户动态的控制函数执行步骤,并交给子类来重写实现。这样我们的代码就有了更多的可能了。
总结
传统语言的模版方法往往需要子父类继承实现,我们把变化的逻辑放置在子类中,父类只保留不变的,也可以内置一个钩子来实现修改父类。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。
当然JavaScript还可以利用高阶函数方式来实现,这会更加的灵活。
享元模式
含义:运用共享技术来有效支持大量细粒度的对象。
该模式主要用于性能上的优化。
工厂生产了男女内衣格50件,但是每件的型号各不相同(不是size,而是style),正常情况需要50个男模特和50个女模特来试穿,每个模特穿一个内衣后进行拍照。但如果以后工厂生产了一万种,那难道还会找一万个模特吗,这显然开销太大了。其实我们考虑内衣大类型上只区分男女信号,所以理论上来讲只需要找一个男模特和一个女模特来穿不同的内衣试穿即可。
所以代码演示如下:
class Model {
constructor(sex) {
this.sex = sex;
}
takePhoto() {
console.log(this.sex + '模特穿' + this.underwear + '号内衣')
}
}
const femaleModel = new Model('female');
const maleModel = new Model('male');
for (let i = 1; i <= 50; i++) {
maleModel.underwear = i
maleModel.takePhoto();
}
for (let i = 1; i <= 50; i++) {
femaleModel.underwear = i;
femaleModel.takePhoto();
}
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量。
对于如何划分内部状态和外部状态,有以下方式:
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这里我们得出上面例子中,性别是内部状态,内衣是外部状态。一般来讲内部状态有几种我们就需要创建几种对象。而无论那种衣服都分有男女,所以其性别是一个可以共享的内部状态,而衣服每件都是不同的,所以划分为外部状态。
对象爆炸
对于文件上传,如果我们选取一个文件就创建一个文件对象进行上传处理,那当遇到大量文件上传的时候是十分消耗性能的。我我有时就会上传上前上万的文件。
let id = 0;
class Upload {
constructor(uploadType, fileName, fileSize) {
this.uploadType = uploadType;
this.fileName = fileName;
this.fileSize = fileSize;
this.dom = null;
}
init(id) {
this.id = id;
this.dom = document.createElement("div");
this.dom.innerHTML = `
<span>文件名称:${this.fileName}, 文件大小:${this.fileSize}</span>
<button class="del-file">删除</button>
`;
this.dom.querySelector(".del-file").onclick = () => {
this.delFile();
};
document.body.appendChild(this.dom);
}
delFile() {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm("确认要删除该文件吗? " + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}
function startUpload(uploadType, files) {
for (let i = 0, file; (file = files[i++]); ) {
const uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
uploadObj.init(id++);
}
}
//分别创建插件上传对象和flash上传对象
startUpload("plugin", [
{
fileName: "1.txt",
fileSize: 1000,
},
{
fileName: "2.txt",
fileSize: 3001,
},
{
fileName: "3.txt",
fileSize: 10000,
},
{
fileName: "4.txt",
fileSize: 2000,
},
]);
startUpload("flash", [
{
fileName: "11.txt",
fileSize: 1000,
},
{
fileName: "22.txt",
fileSize: 3001,
},
{
fileName: "33.txt",
fileSize: 10000,
},
{
fileName: "44.txt",
fileSize: 2000,
},
]);
可以看到我们分为两种上传方式,但是每一种都是添加一个文件,就新建一个上传对象。如果遇到了大量的文件,那势必会造成对象爆炸。
我们接下来可以通过享元模式改善上述代码。
优化对象爆炸
let id = 0;
class Upload {
constructor(uploadType,) {
this.uploadType = uploadType;
}
delFile(id) {
uploadManager.setExternalState(id, this);
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm('确认要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}
// 创建和保存享元对象的工厂类吗,保证一个类型的对象只创建了一个
class UpLoadFactory {
constructor() {
this.createFlyWeightObj = {};
}
create(uploadType) {
if (this.createFlyWeightObj[uploadType]) {
return this.createFlyWeightObj[uploadType]
}
return this.createFlyWeightObj[uploadType] = new Upload(uploadType)
}
}
// 保存外部环境的类
class UploadManager {
constructor() {
this.uploadDatabase = {};
}
add(id, uploadType, fileName, fileSize) {
// 这里创建就只会创建特定类型的上传对象了
let flyWeightObj = upLoadFactory.create(uploadType);
const dom = document.createElement('div');
dom.innerHTML = `
<span>文件名称:${fileName}, 文件大小:${fileSize}</span>
<button class="del-file">删除</button>
`
dom.querySelector('.del-file').onclick = () => {
flyWeightObj.delFile(id);
}
document.body.appendChild(dom);
// 存储到上传管理对象中
this.uploadDatabase[id] = {
fileName,
fileSize,
dom
}
return flyWeightObj;
}
// 给享元对象添加外部状态
setExternalState(id, flyWeightObj) {
const uploadData = this.uploadDatabase[id];
for (const i in uploadData) {
flyWeightObj[i] = uploadData[i]
}
}
}
const uploadManager = new UploadManager();
const upLoadFactory = new UpLoadFactory();
function startUpload(uploadType, files) {
for (let i = 0, file; file = files[i++];) {
uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
}
}
//分别创建插件上传对象和flash上传对象
startUpload('plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.txt',
fileSize: 3001
},
{
fileName: '3.txt',
fileSize: 10000
},
{
fileName: '4.txt',
fileSize: 2000
}
])
startUpload('flash', [
{
fileName: '11.txt',
fileSize: 1000
},
{
fileName: '22.txt',
fileSize: 3001
},
{
fileName: '33.txt',
fileSize: 10000
},
{
fileName: '44.txt',
fileSize: 2000
}
])
console.log(upLoadFactory.createFlyWeightObj);
可以看到我们将上传的一些参数分为了内部状态和外部状态,上传的方式我们作为内部状态,此时就创建了两个对象,而上传的名字,大小,这些参数各个文件都是不同的,并不能共享,所以作为外部状态,当然最后还需要一个关系对象将这两种状态关系起来。通过这样的优化,我们创建的对象始终为两个。
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。
这里以一个地图气泡作为例子,我们使用的地图的上会使用到游戏额tooltips,作为地点的提示,这些tips就可以通过对象池进行管理。比如当我们在第一个地点的时候显示4个tips,而在第二个地点的时候需要显示2个tips,我们切换到第二个地点的时候并不需要重新新建两个dom作为tips,而是从之前创建的四个tips dom取出两个复用即可。
class ToolTipFactory {
constructor() {
this.toolTipPool = []
}
// 获取
create() {
if (this.toolTipPool.length === 0) {
console.log('创建6个div执行了几次');
const div = document.createElement('div');
document.body.appendChild(div);
return div;
} else {
return this.toolTipPool.shift();
}
}
// 回收
recover(tooltipDom) {
return this.toolTipPool.push(tooltipDom)
}
}
const toolTipFactory = new ToolTipFactory();
// 保存已经创建的节点
const ary = []
for (let i = 0, str; str = ['A', 'B'][i++];) {
const toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
ary.push(toolTip);
}
//回收节点
for (let i = 0, dom; dom = ary[i++];) {
toolTipFactory.recover(dom);
}
// 再次创建
for (let i = 0, str; str = ['A1', 'B2', 'C', 'D', 'E', 'F'][i++];) {
const toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
ary.push(toolTip);
}
通用对象池
之前的dom复用对象池我们可以将公共的逻辑抽离出来,编写一个公共的对象池:
class ObjectPoolFactory {
constructor(createObjFn) {
this.objectPool = [];
this.createObjFn = createObjFn;
}
create(...args) {
const obj = this.objectPool.length === 0 ?
this.createObjFn.apply(...args) :
this.objectPool.shift();
return obj;
}
recover(obj) {
this.objectPool.push(obj);
}
}
// 创建一些iframe的对象池
const iframeFactory = new ObjectPoolFactory(function () {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = function () {
iframe.onload = null;
// 加载完成后就回收
iframeFactory.recover(iframe);
}
return iframe;
});
const iframe1 = iframeFactory.create();
iframe1.src = 'http://music.topzhang.cn'
const iframe2 = iframeFactory.create();
iframe2.src = 'http://topzhang.cn';
const iframe3 = iframeFactory.create();
iframe3.src = 'https://altria.topzhang.cn';
// 切换一个展示
setTimeout(() => {
iframe4 = iframeFactory.create();
iframe4.src = "http://music.163.com"
}, 3000)
setTimeout(() => {
console.log(iframeFactory.objectPool);
}, 9000)
总结
享元模式是一种很好的性能优化方案,在以下情况可以使用享元模式进行优化:
- 一个程序中使用了大量的相似对象。
- 使用了大量的对象造成了内存开销过大。
- 对象的大多数状态可以作为外部属性抽离出来。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
对象池其实是一种没有分离内外部状态的性能优化模式,和享元模式相似。所以上传文件的那个案例还可以使用对象池加事件委托来实现。
责任链模式
含义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
计算优惠卷
购物的时候会根据用户是否支付定金,定金数额,来计算给用户优惠多少。这里就许哟啊大量的额条件判断语句进行判断。可能我们第一次想的代码是如下的:
function order(orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log('500元定金,得到优惠卷')
} else {
if (stock > 0) {
console.log('普通购买无优惠卷')
} else {
console.log('没库存')
}
}
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金,得到优惠卷')
} else {
if (stock > 0) {
console.log('普通购买无优惠卷')
} else {
console.log('没库存')
}
}
} else {
if (stock > 0) {
console.log('普通购买无优惠卷')
} else {
console.log('没库存')
}
}
}
order(3, true, 0)
如果我们后续再加上一些其他条件,那这个条件判断会越来越庞大,倒是维护起来容易出错。
责任链模式重构
首先将之前的一些逻辑分离出来,放到一个单独函数中:
function order500(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金')
} else {
order200(orderType, pay, stock)
}
}
function order200(orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金')
} else {
orderNormal(orderType, pay, stock)
}
}
function orderNormal(orderType, pay, stock) {
if (stock > 0) {
console.log("普通购买")
} else {
console.log('没了')
}
}
order500(1, true, 0)
接下来结合责任链进行改造:
function order500(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金')
} else {
return 'next'
}
}
function order200(orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金')
} else {
return 'next'
}
}
function order300(orderType, pay, stock) {
if (orderType === 4 && pay === true) {
console.log('300元定金')
} else {
return 'next'
}
}
function orderNormal(orderType, pay, stock) {
if (stock > 0) {
console.log("普通购买")
} else {
console.log('没了 -----')
}
}
class Chain {
constructor(fn) {
this.fn = fn;
this.successor = null;
}
setNextSuccessor(successor) {
// 指定下一个节点
return this.successor = successor;
}
passRequest(...args) {
// 传递给某个节点
const ret = this.fn(...args);
// 处理不了
if (ret === 'next') {
return this.successor &&
this.successor.passRequest.apply(this.successor, args)
}
return ret;
}
}
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrder300 = new Chain(order300);
const chainOrderNormal = new Chain(orderNormal);
chainOrder500
.setNextSuccessor(chainOrder200)
.setNextSuccessor(chainOrder300)
.setNextSuccessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500)
chainOrder500.passRequest(1, false, 500)
chainOrder500.passRequest(1, false, 0)
chainOrder500.passRequest(2, true, 500)
chainOrder500.passRequest(2, false, 500)
chainOrder500.passRequest(2, false, 0)
chainOrder500.passRequest(4, true, 500)
chainOrder500.passRequest(4, false, 500)
chainOrder500.passRequest(4, false, 0)
chainOrder500.passRequest(3, true, 500)
chainOrder500.passRequest(3, false, 500)
chainOrder500.passRequest(3, false, 0)
现在我们再加上条件的话就不用在一个地方写条件判断了,只需要新增一个逻辑判断函数和连接就行了。
AOP实现责任链
我们还可以结合之前写的AOP函数对责任链对象进行替换:
function order500(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金')
} else {
return 'next'
}
}
function order200(orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金')
} else {
return 'next'
}
}
function order300(orderType, pay, stock) {
if (orderType === 4 && pay === true) {
console.log('300元定金')
} else {
return 'next'
}
}
function orderNormal(orderType, pay, stock) {
if (stock > 0) {
console.log("普通购买")
} else {
console.log('没了 -----')
}
}
Function.prototype.after = function (fn) {
return (...args) => {
const ret = this.apply(null, args);
if (ret === 'next') {
return fn.apply(null, args);
}
return ret;
}
}
const order = order500
.after(order300)
.after(order200)
.after(orderNormal);
order(1, true, 500)
order(1, false, 500)
order(1, false, 0)
order(2, true, 500)
order(2, false, 500)
order(2, false, 0)
order(4, true, 500)
order(4, false, 500)
order(4, false, 0)
order(3, true, 500)
order(3, false, 500)
order(3, false, 0)
总结
责任链的有带你就是解耦了请求者和N个接收者之间的复杂关系,但是职责链也会使
程序中多出一些节点对象,过长的对象链可能会造成性能上的损失。另外,用AOP来实现职责链既简单又巧妙,但这种把函数叠在一起的方式,同时也叠加了函数的作用域,如果链条太长的话,也会对性能也有较大的影响。
中介者模式
含义:解耦对象与对象之间的紧耦合关系,使得多对多关系变成了相对简单的一对多关系。
泡泡堂两人游戏
玩家数量就两个,一个死亡后就会通知自己失败,另一个胜利:
class Player {
constructor(name) {
this.name = name;
this.enemy = null;//敌人
}
win() {
console.log(this.name + ' won!');
}
lose() {
console.log(this.name + ' lost!');
}
die() {
this.lose();
this.enemy.win();
}
}
const player1 = new Player('皮蛋');
const player2 = new Player('小乖');
// 设置敌人
player1.enemy = player2;
player2.enemy = player1;
player1.die();
player2.die()
八人对战的游戏
上面的游戏是两人,但如果我们将队伍加至八人,并且分为两队每队个4人,这时候该如何设置关系:
// 所有的玩家
const players = [];
class Player {
constructor(name, teamColor) {
this.partners = [];
this.enemies = [];
this.state = 'alive';
this.name = name;
this.teamColor = teamColor;//队伍颜色
}
win() {
console.log(this.name + ' win!')
}
lose() {
console.log(this.name + ' lose!')
}
die() {
//需要便利自己队友是否全部死完
let all_dead = true;
this.state = 'dead';
for (let i = 0, partner; partner = this.partners[i++];) {
if (partner.state !== 'dead') {
all_dead = false;
break;
}
}
if (all_dead === true) {
this.lose();
//通知所有队友失败了
for (let i = 0, partner; partner = this.partners[i++];) {
partner.lose();
}
// 通知所有敌人游戏胜利
for (let i = 0, enemy; enemy = this.enemies[i++];) {
enemy.win()
}
}
}
}
// 工厂函数创建玩家并设置关系
function playerFactory(name, teamColor) {
const newPlayer = new Player(name, teamColor);
for (let i = 0, player; player = players[i++];) {
// 有新的角色加入
if (player.teamColor === newPlayer.teamColor) {
//同一队的,添加队友
player.partners.push(newPlayer);
newPlayer.partners.push(player)
} else {
player.enemies.push(newPlayer);
newPlayer.enemies.push(player)
}
}
players.push(newPlayer);
return newPlayer;
}
const player1 = playerFactory('玩家1', 'red')
const player2 = playerFactory('玩家2', 'red')
const player3 = playerFactory('玩家3', 'red')
const player4 = playerFactory('玩家4', 'red')
const player5 = playerFactory('玩家5', 'blue')
const player6 = playerFactory('玩家6', 'blue')
const player7 = playerFactory('玩家7', 'blue')
const player8 = playerFactory('玩家8', 'blue')
player1.die();
player2.die();
player3.die();
player4.die();
我们如果向最开始两人那样手动的去设置关系,那无疑是很麻烦的,所以我们写了一个工厂函数来减少工作量。
中介者模式优化代码
上面的代码虽然看起来还不是很复杂,但是每次死亡等操作都要遍历的通知所有人,如果遇到更多玩家的话那会是很消耗性能的。以下就利用中介者模式进行改造:
// 所有的玩家
const players = [];
// 中介者对象
class PlayDirector {
constructor() {
// 所有玩家
this.players = {};
// 中介者可执行的操作
this.options = {};
}
addPlayer(player) {
const teamColor = player.teamColor;
this.players[teamColor] = this.players[teamColor] ?? [];
this.players[teamColor].push(player);
}
remove(player) {
const teamColor = player.teamColor;
const teamPlayers = this.players[teamColor] ?? [];
for (let i = teamPlayers.length - 1; i >= 0; i--) {
if (teamPlayers[i] === player) {
teamPlayers.splice(i, 1);
}
}
}
changeTeam(player, newTeamColor) {
console.log(player.name, '逃跑到' + newTeamColor + '成功');
this.remove(player);
player.teamColor = newTeamColor;
this.addPlayer(player);
}
playerDead(player) {
const teamColor = player.teamColor;
const teamPlayers = this.players[teamColor];
let all_dead = true;
for (let i = 0, player; player = teamPlayers[i++];) {
// 有一个活的就不算输
if (player.state !== 'dead') {
all_dead = false;
break;
}
}
if (all_dead === true) {
for (let i = 0, player; player = teamPlayers[i++];) {
player.lose();
}
//其他队伍玩家赢
for (const color in this.players) {
if (color !== teamColor) {
const teamPlayers = this.players[color];
for (let i = 0, player; player = teamPlayers[i++];) {
player.win();
}
}
}
}
}
// 封装一个函数用于执行内部方法
reciveMessage(operationType, ...args) {
this[operationType].apply(this, args);
}
}
const playDirector = new PlayDirector();
class Player {
constructor(name, teamColor) {
this.state = 'alive';
this.name = name;
this.teamColor = teamColor;//队伍颜色
}
win() {
console.log(this.name + ' win!')
}
lose() {
console.log(this.name + ' lose!')
}
die() {
this.state = 'dead';
// 给中介者发消息,玩家死亡
playDirector.reciveMessage('playerDead', this)
}
remove() {
// 移除玩家
console.log(this.name, '被移除游戏')
playDirector.reciveMessage('remove', this)
}
changeTeam(teamColor) {
// 玩家换队
playDirector.reciveMessage('changeTeam', this, teamColor)
}
}
// 工厂函数创建玩家
function playerFactory(name, teamColor) {
const newPlayer = new Player(name, teamColor);
// 给中介者发送消息,增加玩家
playDirector.addPlayer(newPlayer)
return newPlayer;
}
const player1 = playerFactory('玩家1', 'red')
const player2 = playerFactory('玩家2', 'red')
const player3 = playerFactory('玩家3', 'red')
const player4 = playerFactory('玩家4', 'red')
const player5 = playerFactory('玩家5', 'blue')
const player6 = playerFactory('玩家6', 'blue')
const player7 = playerFactory('玩家7', 'blue')
const player8 = playerFactory('玩家8', 'blue')
player1.remove();
player2.changeTeam('blue');
player3.die();
player4.die();
购买商品
我们购买商品需要先填写商品的一些参数比如颜色数量,我们选择后再判断库存后才能购买:
// 获取元素
const colorSelect = document.getElementById('colorSelect');
const numberInput = document.getElementById('numberInput');
const colorInfo = document.getElementById('colorInfo');
const numberInfo = document.getElementById('numberInfo');
const nextBtn = document.getElementById('nextBtn');
// 手机库存
const goods = {
red: 3,
blue: 6
}
colorSelect.onchange = function () {
const color = this.value;
const number = numberInput.value;
stock = goods[color];
colorInfo.innerHTML = color;
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
numberInput.oninput = function () {
const color = colorSelect.value;
number = this.value;
stock = goods[color];
numberInfo.innerHTML = number;
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<select id="colorSelect">
<option value="">请选择</option>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
输入购买数量:
<input type="text" id="numberInput">
<br>
<hr>
您选择的颜色:
<div id="colorInfo">
</div>
您输入的数量:
<div id="numberInfo">
</div>
<hr>
<hr>
<button id="nextBtn" disabled>请选择手机颜色和数量</button>
<script src="./4-购物选择模块.js"></script>
</body>
</html>
当我们选择或者输入的时候都会区判断这些逻辑,这看起来十分的臃肿。如果我们后续又添加了一个参数,比如手机的内存选择,这又需要改动内部的代码:
// 获取元素
const colorSelect = document.getElementById('colorSelect');
const numberInput = document.getElementById('numberInput');
const memorySelect = document.getElementById('memorySelect');
const colorInfo = document.getElementById('colorInfo');
const numberInfo = document.getElementById('numberInfo');
const memoryInfo = document.getElementById('memoryInfo');
const nextBtn = document.getElementById('nextBtn');
// 手机库存
const goods = {
'red|32G': 3,
"red|16G": 0,
'blue|32G': 1,
"blue|16G": 6,
}
colorSelect.onchange = function () {
const color = colorSelect.value;
const number = numberInput.value;
const memory = memorySelect.value;
stock = goods[color + '|' + memory];
colorInfo.innerHTML = color;
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存';
return;
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
numberInput.oninput = function () {
const color = colorSelect.value;
const number = numberInput.value;
const memory = memorySelect.value;
const stock = goods[color + '|' + memory];
numberInfo.innerHTML = number;
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存';
return;
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
memorySelect.onchange = function () {
const color = colorSelect.value;
const number = numberInput.value;
const memory = memorySelect.value;
stock = goods[color + '|' + memory];
numberInfo.innerHTML = number;
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存';
return;
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
console.log(stock);
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
颜色:
<select id="colorSelect">
<option value="">请选择</option>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
内存:
<select id="memorySelect">
<option value="">请选择</option>
<option value="32G">32G</option>
<option value="16G">16G</option>
</select>
输入购买数量:
<input type="text" id="numberInput">
<br>
<hr>
您选择的颜色:
<div id="colorInfo">
</div>
您选择的内存:
<div id="memoryInfo">
</div>
您输入的数量:
<div id="numberInfo">
</div>
<hr>
<hr>
<button id="nextBtn" disabled>请选择手机颜色和数量</button>
<!-- <script src="./5-购物车改动.js"></script> -->
<script src="./6-引入中介者模式解决购物车问题.js"></script>
</body>
</html>
可以看到我们加了一个条件后就多了这么多代码。
中介者模式改造代码
我们可以将上述公共代码放到中介者对象中进行复用:
// 获取元素
// 手机库存
const goods = {
'red|32G': 3,
"red|16G": 0,
'blue|32G': 1,
"blue|16G": 6,
}
class Mediator {
constructor() {
this.colorSelect = document.getElementById('colorSelect');
this.numberInput = document.getElementById('numberInput');
this.memorySelect = document.getElementById('memorySelect');
this.colorInfo = document.getElementById('colorInfo');
this.numberInfo = document.getElementById('numberInfo');
this.memoryInfo = document.getElementById('memoryInfo');
this.nextBtn = document.getElementById('nextBtn');
}
changed(obj) {
const color = this.colorSelect.value;
const number = this.numberInput.value;
const memory = this.memorySelect.value;
const stock = goods[color + '|' + memory];
if (obj === this.olorSelect) {
this.colorInfo.innerHTML = color;
} else if (obj === this.memorySelect) {
console.log(memory);
this.memoryInfo.innerHTML = memory;
} else if (obj === this.numberInput) {
this.numberInfo.innerHTML = number;
}
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存';
return;
}
if (!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购物数量';
return;
}
if (parseInt(number) > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
}
const mediator = new Mediator();
colorSelect.onchange = function () {
mediator.changed(this)
}
numberInput.oninput = function () {
mediator.changed(this)
}
memorySelect.onchange = function () {
mediator.changed(this)
}
总结
中介者模式是符合最少知识原则,使得对象之间的关系不在过于耦合,但是也有一定的确定啊就是会使得系统中多一个中介者对象。
在实际项目中,模块或对象之间有一些依赖关系是很正常的。如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
装饰器模式
含义:给对象动态地增加职责。
传统语言的装饰器
class Plane {
fire() {
console.log('发射普通子弹')
}
}
class MissileDecorator {
constructor(plane) {
this.plane = plane;
}
fire() {
this.plane.fire();
console.log('发射导弹')
}
}
class AtomDecorator {
constructor(plane) {
this.plane = plane;
}
fire() {
this.plane.fire();
console.log('发射原子弹')
}
}
let plane = new Plane();
plane = new MissileDecorator(plane);
plane.fire();
plane = new AtomDecorator(plane);
plane.fire();
JavaScript实现装饰器
对于JS来说实现装饰器是非常容易的,且并不需要类来实现:
const plane = {
fire() {
console.log('发射普通子弹')
}
}
const missileDecorator = function () {
console.log('发射导弹')
}
const atomDecorator = function () {
console.log('发射原子弹')
}
let fire1 = plane.fire;
plane.fire = function () {
fire1();
missileDecorator();
}
plane.fire();
let fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
}
plane.fire();
AOP实现装饰器
上面我们通过重写函数来达到装饰器的功能,这是违反开放封闭原则的,所以我们可以借助原型链来进一步改造:
Function.prototype.before = function (beforeFn) {
const __self = this;
return function (...args) {
beforeFn.apply(this, args);
return __self.apply(this, args)
}
}
Function.prototype.after = function (afterFn) {
const __self = this;
return function (...args) {
const ret = __self.apply(this, args);
afterFn.apply(this, args);
return ret;
}
}
// document.getElementById = document.getElementById.before(function () {
// alert(1);
// })
const button = document.getElementById('button');
console.log(button)
window.onload = function () {
alert(1);
}
window.onload = (window.onload || function () { }).after(function () {
alert(2);
}).after(function () {
alert(3)
}).after(function () {
alert(4)
})
当然如果你不喜欢再原型链上加一些函数你可以使用以下方案:
function after(fn, afterFn) {
return function (...args) {
const ret = fn.apply(this, args);
afterFn.apply(this, args);
return ret;
}
}
function before(fn, beforeFn) {
return function (...args) {
// 这里共用一个属性对象所以可以修改args动态给原函数扩展参数
beforeFn.apply(this, args);
return fn.apply(this, args);
}
}
// document.getElementById = document.getElementById.before(function () {
// alert(1);
// })
const button = document.getElementById('button');
console.log(button)
window.onload = function (params) {
//alert(1);
console.log(params);
}
window.onload = before((window.onload || function (params) { }), function (params) {
params.a = 11111;
})
总结
装饰器模式使用实例也非常广泛,其中AOP的装饰器技巧在实际的开发中非常有用,比如我们可以进行数据上报,在某些操作后上报数据,再比如动态修改函数的参数(参数是个对象)。
状态模式
电灯程序
class Light {
constructor() {
this.state = 'off';
this.button = null;
}
init() {
const button = document.createElement('button');
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.button.onclick = () => {
this.buttonWasPress();
}
}
buttonWasPress() {
if (this.state === 'off') {
console.log('开灯');
this.state = 'on';
} else if (this.state === 'on') {
console.log('关灯');
this.state = 'off';
}
}
}
const light = new Light();
light.init();
上述代码能完美的实现功能,但是切换状态的函数内部存在比较死板的条件判断。如果我们不止两种状态的话又会去堆砌条件判断语句。
状态模式改进
class Light {
constructor() {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.superStrongLightState = new SuperStrongLightState(this);
this.button = null;
}
init() {
const button = document.createElement('button');
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.currentState = this.offLightState;
this.button.onclick = () => {
this.currentState.buttonWasPressed();
}
}
setState(newState) {
this.currentState = newState
}
}
class OffLightState {
constructor(light) {
this.light = light
}
buttonWasPressed() {
console.log('弱光');
this.light.setState(this.light.weakLightState);
}
}
class WeakLightState {
constructor(light) {
this.light = light
}
buttonWasPressed() {
console.log('强光');
this.light.setState(this.light.strongLightState);
}
}
class StrongLightState {
constructor(light) {
this.light = light
}
buttonWasPressed() {
console.log('超强光');
this.light.setState(this.light.superStrongLightState);
}
}
class SuperStrongLightState {
constructor(light) {
this.light = light
}
buttonWasPressed() {
console.log('关灯');
this.light.setState(this.light.offLightState);
}
}
const light = new Light();
light.init();
我们把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在的大量条件分支语句。
JS版状态机
function delegate(client, delegation) {
return {
buttonWasPressed(...args) {
return delegation.buttonWasPressed.apply(client, args)
}
}
}
const FSM = {
offLightState: {
buttonWasPressed() {
console.log('关灯');
this.currentState = this.onLightState;
}
},
onLightState: {
buttonWasPressed() {
console.log('开灯');
this.currentState = this.offLightState;
}
}
}
class Light {
constructor() {
this.offLightState = delegate(this, FSM.offLightState);
this.onLightState = delegate(this, FSM.onLightState);
this.currentState = this.offLightState;
this.button = null;
}
init() {
const button = document.createElement('button');
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.button.onclick = () => {
this.currentState.buttonWasPressed();
}
}
setState(newState) {
this.currentState = newState
}
}
const light = new Light();
light.init();
还有一种表驱动的状态机,详见javascript-state-machine
总结
状态模式优点如下:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
- Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
参考文献: