小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
记录自己学习设计模式,内容来自
《JavaScript设计模式与开发实践》
迭代模式的定义
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素.
实现简单的迭代器
const each = function(arr, callback) {
for (let i = 0, l = arr.length; i < l; i ++) {
callback.call(arr[i], arr[i], i)
}
}
each([1, 2, 3], (item, i) => {
alert([item, i])
})
内部迭代器和外部迭代器
迭代器可以分为外部迭代器和内部迭代器, 它们都有各自的适用场景
内部迭代器
上面的each函数属于内部迭代器,each函数的内部定义好了迭代规则,它完全的接手了整个迭代过程,外部只需要一次初始调用
内部迭代器在调用的时候非常方便; 外界不用关心迭代器内部的实现, 跟迭代器的交互也仅是一次初始调用,但这也刚好是内部迭代器的缺点. 由于内部迭代器的迭代规则已经被提前规定, 上面的each函数就无法同时迭代两个数组了.
比如现在有个需求, 要判断两个数组里元素的值是否相等, 如果不改写each函数本身的代码, 我们能够入手的地方只剩下each的回调函数了
const compare = function (ary1, ary2) {
if (ary1.length !== ary2.length) {
throw new Error('ary1和ary2不相等')
}
each(ary1, (item, i) => {
if (item !== ary2[i]) {
throw new Error('ary1和ary2不相等')
}
})
alert('ary1和ary2相等')
}
compare([1, 2, 3], [1, 2, 4]) // 不相等
说实话, 这个compare函数很low, 我们能够顺利完成需求, 还要归功于Js里可以把函数当参数传递的特性, 但在其他语言未必可以.
在一些没有闭包的语言代理中, 内部迭代器本身的实现也相当复杂. 比如C语言中的内部迭代器是用函数指针来实现的, 循环处理所需要的数据都要以参数的形式明确地从外面传递进去
外部迭代器
外部迭代器必须显式的请求迭代下一个元素
外部迭代器增加了一些调用的复杂度, 但相对也增强了迭代器的灵活性, 我们可以手工控制迭代的过程或者顺序.
下面这个外部迭代器的实现来自<松本行弘的程序世界>第四章, 原例用Ruby, 这里换成Js
const iterator = function (obj) {
let current = 0
const next = () => current += 1
const isDone = () => current >= obj.length
const getCurrItem = () => obj[current]
return {
next,
isDone,
getCurrItem,
length: obj.length
}
}
// 接下来改写compare函数
const compare = function(iterator1, iterator2) {
if (iterator1.length !== iterator2.length) {
console.log('不相等')
}
while (!iterator1.isDone() && !iterator2.isDone()) {
if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
throw new Error('不相等')
}
iterator1.next()
iterator2.next()
}
console.log('相等')
}
const iterator1 = iterator([1, 2, 3])
const iterator2 = iterator([1, 2, 3])
compare(iterator1, iterator2)
外部迭代器虽然调用方式复杂, 但它的适用面更广, 也能满足更多变得需求. 内部迭代器和外部迭代器无优劣之分, 使用哪个根据需求场景而定
迭代类数组和字面量对象
$.each = function(obj, callback) {
let valie,
i = 0,
length = obj.length,
isArray = Array.isArray(obj)
if (isArray) {
for(; i < length; i++) {
value = callback.call(obj[i], i, obj[i])
if (value === false) {
break;
}
}
} else {
for( i in obj ) {
value = callback.call(obj[i], i, obj[i])
if (value === false) {
break;
}
}
}
return obj
}
倒序迭代器
const reverseEach = function(ary, callback) {
for(let l = ary.length - 1; l >= 0; l--) {
callback(l, ary[l])
}
}
中止迭代器
const each = function(ary, callback) {
for(let i = 0, l = ary.length; i < l; i++) {
if(callback(i, ary[1]) === false) {
break
}
}
}
each([1, 2, 3, 4, 5], function(i, n) {
if (n > 3) {
return false
}
console.log(n)
})
应用
来看一段代码
const getUploadObj = function() {
try {
return new ActiveXObject('TXFTNActiveX.FTNUpload') // IE上传控件
} catch (e) {
if (supportFlash()) { // 未提供这个函数, 是否支持flash
const str = `<object type="application/x-shockwave-flash"></object>`
return $(str).appendTo($('body'))
} else {
const str = '<input name="file" type="file" />' // 表单上传
return $(str).appendTo($('body'))
}
}
}
在不同的浏览器环境下, 选择的上传方式是不一样的. 因为使用浏览器上传控件进行上传速度快, 可以暂停和续传, 所以我们优先选择控件上传. 如果浏览器没有安装上传控件, 则使用flash上传, 如果连flash也没有, 则使用浏览器原生的表单上传
看看上面的代理, 为了得到一个upload对象, 这个getUploadObj 函数里面充斥了try, catch以及if条件分支. 缺点显而易见的
- 很难阅读
- 违反开闭原则
- 拓展性差
在开发和调试过程中, 我们需要来回切换不同的上传方式, 每次改动都相当痛苦. 后来我们还支持了一些另外的上传方式, 比如, HTML5上传, 这时候唯一的办法就是继续往getUploadObj函数里增加条件分支.
来梳理一下, 目前一共有3种可能的上传方式, 我们不知道目前正在使用的浏览器支持哪几种. 就好比有3把钥匙, 我们想打开一扇门不知道该使用哪把. 于是从第一把开始, 迭代钥匙串进行尝试, 直到找到了正确的钥匙为止.
const getActiveUploadObj = function() {
try {
return new ActionXObject('TXFTNActiveX.FTNUpload')
} catch(e) {
return false
}
}
const getFlashUploadObj = function() {
if (supportFlash()) { // 未提供这个函数, 是否支持flash
const str = `<object type="application/x-shockwave-flash"></object>`
return $(str).appendTo($('body'))
}
return false
}
const getFormUploadObj = function() {
const str = '<input name="file" type="file" />' // 表单上传
return $(str).appendTo($('body'))
}
// 这三个函数都有同一个约定, 如果该函数里面的upload对象是可用的,则让函数返回该对象, 否则返回false, 提示迭代器继续迭代
// 1.提供一个可以被迭代的方法, 使用三个方法 依照优先级被循环迭代
// 2. 如果正在被迭代的函数返回一个函数,则表示找到了upload的对象, 反之函数返回false,则继续迭代
const iteratorUploadObj = function() {
for(let i = 0, fn; fn = arguments[i++]) {
const uploadObj = fn()
if (uploadObj !== false) {
return uploadObj
}
}
}
const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, iteratorUploadObj)
重构代码之后, 我们可以看到, 获取不同上传对象的方法被隔离在各自的函数里互不干扰, try, catch 和 if分支 不在纠缠在一起, 使得我们可以很方便的维护和拓展.
比如, 后来我们增加了webkit控件上传和html5上传, 我们要做的仅仅是下面的一些工作
增加如下函数
const getWebkitUploadObj = function() {
// ...代码略
}
const getHtml5UploadObj = function() {
// ...代码略
}
// 依靠优先级添加进迭代器
const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, iteratorUploadObj, getWebkitUploadObj, getHtml5UploadObj)
小结
迭代器是一种相对简单的模式, 简单到很多时候我们都不认为它是一种设计模式. 目前的绝大部分语言都内置了迭代器(ES6迭代器, for of)