JavaScript设计模式与开发事件阅读总结(二)
设计模式的主题:将不变的部分和变化的部分隔开
一、单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
实现原理:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
重点:创建对象和管理单例的职责被分布在两个不同的方法中。
应用场景:
- 一个网站的登录,点击登录后弹出一个登录弹框,即使再次点击,也不会再出现一个相同的弹框。
- 一个音乐播放程序,如果用户打开了一个音乐,又想打开一个音乐,那么之前的播放界面就会自动关闭,切换到当前的播放界面。
var Singleton = function (name) {
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function () {
alert(this.name);
};
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
var a = Singleton.getInstance('sven1');
var b = Singleton.getInstance('sven2');
alert(a === b); // true
var Singleton = function (name) {
this.name = name;
};
Singleton.prototype.getName = function () {
alert(this.name);
};
Singleton.getInstance = (function () {
var instance = null;
return function (name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
};
})();
// 注意:立即执行函数只会执行一次,所有第二次调用此方法不会走var instance = null;这条语句
var a = Singleton.getInstance('sven1'); // 会执行Singleton.getInstance整个函数体
var b = Singleton.getInstance('sven2'); // 只会执行return后面的这个函数
alert(a === b); // true
代理实现单例模式:把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv 中,CreateDiv 就变成了一个普通的类。
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('sven1');
var b = new ProxySingletonCreateDiv('sven2');
alert(a === b);
降低全局变量带来的命名污染的方法:使用命名空间、使用闭包封装私有变量。
惰性单例:在需要的时候才创建对象实例。实例对象总是在我们调用某个方法的时候才被创建。
// 管理单例的逻辑封装在getSingle函数内部
var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
};
};
// 创建唯一的登录框
var createLoginLayer = function () {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
二、策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
优点:
- 利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句;
- 提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它
们易于切换,易于理解,易于扩展; - 利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻
便的替代方案。
js 策略模式
var strategies = {
S: function (salary) {
return salary * 4;
},
A: function (salary) {
return salary * 3;
},
B: function (salary) {
return salary * 2;
}
};
var calculateBonus = function (level, salary) {
return strategies[level](salary);
};
console.log(calculateBonus('S', 20000)); // 输出:80000
console.log(calculateBonus('A', 10000)); // 输出:30000
表单验证策略模式实现:
// 策略对象
var strategies = {
isNonEmpty: function (value, errorMsg) {
// 不为空
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
// 限制最小长度
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
// 手机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
// Validator 类
var Validator = function () {
this.cache = []; // 保存校验规则
};
Validator.prototype.add = function (dom, rule, errorMsg) {
var ary = rule.split(':'); // 把 strategy 和参数分开
this.cache.push(function () {
// 把校验的步骤用空函数包装起来,并且放入 cache
var strategy = ary.shift(); // 用户挑选的 strategy
ary.unshift(dom.value); // 把 input 的 value 添加进参数列表
ary.push(errorMsg); // 把 errorMsg 添加进参数列表
return strategies[strategy].apply(dom, ary);
});
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; (validatorFunc = this.cache[i++]); ) {
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if (msg) {
// 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};
// 客户调用代码
var validataFunc = function () {
var validator = new Validator(); // 创建一个 validator 对象
/***************添加一些校验规则****************/
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add(registerForm.password, 'minLength:6', '密码长度不能少于 6 位');
validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
};
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
if (errorMsg) {
alert(errorMsg);
return false; // 阻止表单提交
}
};
三、代理模式
定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
理解:当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
代理保护:用于控制不同权限的对象对目标对象的访问。
虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。
虚拟代理实现图片预加载:
先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里。
// 负责给 img 节点设置 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('file:// /C:/Users/svenzeng/Desktop/loading.gif');
img.src = src;
}
};
})();
proxyImage.setSrc('http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg');
单一职责原则:就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。
虚拟代理合并 HTTP 请求:
实际场景:当我们选中一个 checkbox 的时候,它对应就要向服务器发送一次请求,如此频繁的网络请求将会带来相当大的开销。
解决:通过一个代理函数来收集一段时间之内的请求,最后一次性发送给服务器。
var synchronousFile = function (id) {
console.log('开始同步文件,id 为: ' + id);
};
var proxySynchronousFile = (function () {
var cache = [], // 保存一段时间内需要同步的 ID
timer; // 定时器
return function (id) {
cache.push(id);
if (timer) {
// 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function () {
synchronousFile(cache.join(',')); // 2 秒后向本体发送需要同步的 ID 集合
clearTimeout(timer); // 清空定时器
timer = null;
cache.length = 0; // 清空 ID 集合
}, 2000);
};
})();
var checkbox = document.getElementsByTagName('input');
for (var i = 0, c; (c = checkbox[i++]); ) {
c.onclick = function () {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
};
}
缓存代理:为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
缓存代理用于求乘积:
var proxyMult = (function () {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ',');
// 实现缓存的主要代码
if (args in cache) {
return cache[args];
}
return (cache[args] = mult.apply(this, arguments));
};
})();
缓存代理的第二个应用场景:ajax 异步请求数据
遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。
四、迭代器模式
定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
js 内置了迭代器:forEach 迭代器、every 迭代器、some 迭代器、reduce 迭代器、map 迭代器和 fiter 迭代器。
使用场景:操作日志
五、发布-订阅模式(观察者模式)
定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
实现代码:
// 将方法暴露在全局使用
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn); // 订阅的消息添加进缓存列表
};
trigger = function () {
var key = Array.prototype.shift.call(arguments), // (1);
fns = clientList[key];
if (!fns || fns.length === 0) {
// 如果没有绑定对应的消息
return false;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
// 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) {
// 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) {
// 反向遍历订阅的回调函数列表
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1); // 删除订阅者的回调函数
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
};
})();
fn = function (price) {
console.log('价格= ' + price); // 输出:'价格=2000000'
};
Event.listen('squareMeter88', fn); // 订阅消息
Event.trigger('squareMeter88', 2000000); // 发布消息
Event.remove('squareMeter88'); // 解绑
六、模板方法模式
定义:由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。