《JavaScript设计模式》----张荣铭(四)
首先说一下什么是设计模式?以及我们为什么要学习设计模式?
设计模式的定义是:设计模式是在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
也可以通俗的理解为:设计模式在某个特定场景下都某种问题的解决方案。当然,这也就是为什么我们要学习设计模式的原因。本书将设计模式按照类型分成六大类
- 创建型设计模式
- 结构型设计模式
- 行为型设计模式
- 技巧型设计模式
- 架构型设计模式
4 技巧型设计模式
4.1 链模式
通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用时,对该对象的多次引用。
例如使用d3的库时,经常会用到以下的写法
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x",20)
.attr("y",function(d,i){
return i * rectHeight;
})
.attr("width",function(d){
return linear(d);
})
.attr("height",rectHeight-2)
.attr("fill","steelblue")
其实链模式的核心思想就是每次调用完某个API时,将当前对象,this返回
function Chain() {
this.container = "";
this.attr = function (type, value) {
this.container.style[type] = value;
return this;
};
this.text = function (value) {
this.container.text = value;
return this;
};
this.on = function (eventType, fn) {
this.container.addEventListener(eventType, fn);
return this;
};
this.select = function (element) {
this.container = document.getElementById(element);
return this;
};
}
var chain = new Chain();
chain.select('ele')
.attr("background", "red")
.text("链式调用方法测试!")
.on("click", () => {
alert("你点击了我!还一笑而过");
});
4.2 委托模式
委托模式:多个对象接受并处理同一请求,他们将请求委托给另一个对象统一请求。
比如一个页面中,需要给<ul>
标签下的所有<li>
标签添加click事件,我们大可不必给每一个<li>
都添加click事件,这样会造成很大的性能浪费。可以将这多个请求委托给更高层的<ul>
元素去执行绑定。
// 给li的外层元素ul添加事件
var ul = document.getElementById('entrust');
ul.onclick = function(e) {
var e = e || window.event;
var target = e.target || e.srcElement;
if (target.nodeName.toLowerCase() === 'li') {
target.style.background = 'red';
target.innerText = 'copy that!';
}
}
4.3 数据访问对象模式
数据访问对象模式就是对数据源的访问与存储进行封装,提供一个数据访问对象类负责对存储的数据进行管理和操作,规范数据存储格式,类似于后台的DAO层。
举个例子,对一个大型的web应用来说,其存储在WebStorage中的数据可能会非常的多,当多个程序员同时开发时候,会遇到一个问题:怎么保证自己存储的数据会不会覆盖其他人的数据,并且在删除Storage中的数据时会不会删除其他人的数据?这时候可以用数据访问对象模式来解决。
由于WebStorage中以key-value来存储,所以我们需要对key的格式进行规范,比如模块名+key,或者开发人员+key,还可以在key之前添加一段前缀来描述数据(最近的碰到的项目中也有在所有Reducer中新增一个Prefix的做法),还可以在值中添加一段前缀来描述数据,如添加数据过期日期的时间戳,用来管理生命周期。具体格式项目组
下面,以localStorage为例,介绍一下数据访问对象模式的使用方法。
/**
* LocalStorage数据访问类
* @param {string} prefix Key前缀
* @param {string} timeSplit 时间戳与存储数据之间的分割符
*/
var DAO = function (prefix, timeSplit) {
this.prefix = prefix;
this.timeSplit = timeSplit;
};
DAO.prototype = {
// 操作状态
status: {
SUCCESS: 0,
FAILURE: 1, // 失败
OVERFLOW: 2, // 溢出
TIMEOUT: 3 // 过期
},
// 本地存储对象
storage: localStorage || window.localStorage,
// 获取 加上prefix的 真实key
getKey: function (key) {
return this.prefix + key;
},
// 添加(修改)数据
set: function (key, value, callback, time) {
var status = this.status.SUCCESS;
var key = this.getKey(key);
try {
// 设置时间
time = new Date(time).getTime() || time.getTime();
} catch (e) {
// 设置失败之后默认为当前时间的后一个月
time = new Date().getTime() + 1000 * 60 * 60 * 24 * 30;
}
try {
this.storage.setItem(key, time + this.timeSplit + value)
} catch (e) {
// localStorage 的最大存储量为5M,可能存在溢出的情况
status = this.status.OVERFLOW;
}
callback && callback.call(this, status, key, value);
},
get: function (key, callback) {
var key = this.getKey(key);
var value = null;
try {
value = this.storage.getItem(key);
} catch (e) {
status = this.status.FAILURE;
value = null;
}
if (status !== this.status.FAILURE) {
var index = value.indexOf(this.timeSplit);
time = value.slice(0, index);
if (new Date(1 * time).getTime() > new Date().getTime() || time == 0) {
// 获取数据值
value = value.slice(index + this.timeSplit.length);
} else {
value = null;
status = this.status.TIMEOUT;
this.remove(key);
}
}
callback && callback.call(this, status, value);
return value;
},
remove: function () {
// 此处省略N行
}
}
var zaorenDAO = new DAO('zaoren', '--');
zaorenDAO.set('zaorenRealName', 'wwq');
console.log(zaorenDAO.get('zaorenRealName'));
4.4 节流模式
节流模式(Throttler):对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能。
除了节流,与之相似的还有防抖(Debounce)操作。
防抖: 任务在被频繁触发的情况下,只有任务触发的间隔超过指定的间隔的时候,任务才会执行
节流: 指定时间间隔内只会执行一次
var throttle = function () {
var isClear = arguments[0];
var fn;
if (typeof isClear === 'boolean') {
fn = arguments[1];
// 函数的计时器句柄存在,则清除该计时器
fn.__throttleID && clearTimeout(fn, __throttleID);
} else {
fn = isClear;
param = arguments[1];
var p = {
context: null,
args: [],
time: 1000,
...param
};
// 清除执行函数 计时器句柄
arguments.callee(true, fn);
fn.__throttleID = setTimeout(function () {
fn.apply(p.context, p.args);
}, p.time)
}
}
var button = document.getElementById('button');
button.onclick = () => {
throttle(() => {
console.log('节流节流!');
})
}
(参考这本书上的节流实现方法,我试了下,总感觉只是把所有动作延时执行了,并没有起到节流的效果)
function debounce(fn) {
let timeoutId = null;
retrun function() {
clearTimeOut(timeoutId);
timeoutId = setTimtOut(() => {
fn.call(this, arguments)
}, 1000)
}
}
function throttle(fn) {
let canRun = true;
return function () {
if (!canRun) { // 在函数开头判断标志是否为 true,不为 true 则中断函数
return;
}
canRun = false;
setTimeout(() => {
fn.call(this);
canRun = true;
}, 1000)
}
}
节流模式(Throttler):对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能。
除了节流,与之相似的还有防抖(Debounce)操作。
防抖: 任务在被频繁触发的情况下,只有任务触发的间隔超过指定的间隔的时候,任务才会执行
节流: 指定时间间隔内只会执行一次
var throttle = function () {
var isClear = arguments[0];
var fn;
if (typeof isClear === 'boolean') {
fn = arguments[1];
// 函数的计时器句柄存在,则清除该计时器
fn.__throttleID && clearTimeout(fn, __throttleID);
} else {
fn = isClear;
param = arguments[1];
var p = {
context: null,
args: [],
time: 1000,
...param
};
// 清除执行函数 计时器句柄
arguments.callee(true, fn);
fn.__throttleID = setTimeout(function () {
fn.apply(p.context, p.args);
}, p.time)
}
}
var button = document.getElementById('button');
button.onclick = () => {
throttle(() => {
console.log('节流节流!');
})
}
(参考这本书上的节流实现方法,我试了下,总感觉只是把所有动作延时执行了,并没有起到节流的效果)
下面给出简单的防抖节流的实现方法,另外平时项目中可以直接引入lodash的debounce和throttle
function debounce(fn) {
let timeoutId = null;
retrun function() {
clearTimeOut(timeoutId);
timeoutId = setTimtOut(() => {
fn.call(this, arguments)
}, 1000)
}
}
function throttle(fn) {
let canRun = true;
return function () {
if (!canRun) { // 在函数开头判断标志是否为 true,不为 true 则中断函数
return;
}
canRun = false;
setTimeout(() => {
fn.call(this);
canRun = true;
}, 1000)
}
}
4.5 简单模板模式
简单模板模式:通过格式化字符串拼凑出视图避免创建视图时大量节点操作,优化内存开销。
暂不举例说明
4.6 惰性模式
惰性模式:减少每次代码执行时的重复性的分支判断,通过对对象冲定义来屏蔽元对象中的分支判断。
实现惰性模式有两种方法:
- 文件加载后立即执行对象方法来重定义对象。
- 第二种是当第一次使用方法对象时重定义对象。
// 实现惰性模式: 方法一 文件刚加载的时候重定义
var createXHR = (function () {
if (typeof XMLHttpRequest !== 'undefined') {
return function () {
new XMLHttpRequest();
}
} else if (typeof ActiveXObject != 'undefined') {
return function () {
// 省略代码
}
} else {
throw new Error('No XHR Obj avaliable!');
}
})()
// 实现惰性模式: 方法二 执行的时候重定义
function createXHR() {
if (typeof XMLHttpRequest !== 'undefined') {
createXHR = function () {
return new XMLHttpRequest();
}
} else if (typeof ActiveXObject !== 'undefined') {
createXHR = function () {
// 省略代码
}
} else {
throw new Error('No XHR Obj avaliable!');
}
return createXHR();
}
4.7 参与者模式
在特定的作用域中执行给点给点函数,并将参数原封不动地传递。
let A = { event: {} }
A.event.on = function (dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener(type, fn, false)
} else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn)
} else {
dom['on' + type] = fn
}
}
// 难点 addEventListener 不能传入data
// 解决 在回调函数里面做文章
A.event.on = function (dom, type, fn, data) {
if (dom.addEventListener) {
dom.addEventListener(type, function (e) {
fn.call(dom, e, data)
})
}
}
// 新问题: 添加的事件回调函数不能移除了
// 解决: bind apply改变this apply 小demo
function bind(fn, context) {
return function () {
return fn.apply(context, arguments)
}
}
var demoObj = {
title: '这是一个例子',
}
function demoFn() {
console.log(this.title)
}
var bindFn = bind(demoFn, demoObj)
bindFn() // 这是一个例子
var btn = document.getElementsByTagName('button')[0];
var p = document.getElementsByTagName('p')[0]
//改造
function demoFn() {
console.log(arguments, this)
}
var bindFn = bind(demoFn)
btn.addEventListener('click', bindFn) // [MouseEvent] Window {external: Object, chrome: Object, document: document, demoObj: Object, btn: button…}
bindFn = bind(demoFn, btn)
btn.addEventListener('click', bindFn) // [MouseEvent] <button>click me</button>
// 有些高级浏览器有提供bind函数 实现如下
var bindFn = demoFn.bind(demoObj)
/**
* 函数柯里化: 对函数的参数分割, 类似于多态
*/
function curry(fn) {
//缓存数据slice方法
var slice = [].slice
var args = slice.call(arguments, 1);
return function () {
var addArgs = slice.call(arguments),
allArgs = args.concat(addArgs)
return fn.apply(null, allArgs)
}
}
function add(num1, num2) {
return num1 + num2
}
function add5(num) {
return 5 + num
}
//用curry实现两种加法 函数的创建过程在curry里实现了
let add7 = curry(add, 7, 8)
let add58 = curry(add, 5)
//重写bind
function bind(fn, context) {
var slice = Array.prototype.slice,
args = slice.call(arguments, 2)
return function () {
var addArgs = slice.call(arguments),
allArgs = addArgs.concat(args);
return fn.apply(context, allArgs)
}
}
// 测试
var demoData1 = {
text: '这是第一组数据'
},
demoData2 = {
text: '第二个数据'
}
bindFn = bind(demoFn, btn, demoData1)
btn.addEventListener('click', bindFn) // [MouseEvent, Object<demoData1>] <button>click me</button>