Skip to content

javascript设计模式 - 迭代器模式 #4

@mhahaha

Description

@mhahaha

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

###实现自己的迭代器
现在我们来自己实现一个 each 函数,each 函数接受 2 个参数,第一个为被循环的数组,第二个为循环中的每一步后将被触发的回调函数:

const each = (ary, callback) => {
    for (let i = 0, l = ary.length; i < l; i++) {
        // 把下标和元素当作参数传给 callback 函数
        callback.call(ary[i], i, ary[i])
    }
}
each([1, 2, 3], (i, n) => {
    alert([i, n])
})

内部迭代器和外部迭代器

迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景。这一节我们将分别讨论这两种迭代器。

1.内部迭代器

上面刚写的each就是内部迭代器,它内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。这也暴露出内部迭代器的缺点,即只能按照迭代器内部的规则进行迭代,例如each不能同时迭代两个数组

2.外部迭代器

  • 外部迭代器必须显式地请求迭代下一个元素。
  • 外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。

以“判断两数组是否相等”这个需求为例,加深对内部迭代器与外部迭代器的理解。

【内部迭代器实现】

const compare = (ary1, ary2) => {
    if (ary1.length !== ary2.length) {
        throw new Error('ary1 和 ary2 不相等')
    }
    each(ary1, (i, n) => {
        if (n !== ary2[i]) {
            throw new Error('ary1 和 ary2 不相等')
        }
    })
    console.log('ary1 和 ary2 相等')
}
compare([1, 2, 3], [1, 2, 4]) // throw new Error ( 'ary1 和 ary2 不相等' )

【外部迭代器实现】

const Iterator = obj => {
    let current = 0
    const next = () => {
        current++
    }
    const isDone = () => current >= obj.length
    const getCurrItem = () => obj[current]

    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem
    }
}

const compare = (iterator1, iterator2) => {
    while (!iterator1.isDone() && !iterator2.isDone()) {
        if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
            throw new Error('iterator1 和 iterator2 不相等')
        }
        iterator1.next()
        iterator2.next()
    }
    console.log('iterator1 和 iterator2 相等')
}
const iterator1 = Iterator([1, 2, 3])
const iterator2 = Iterator([1, 2, 3])
compare(iterator1, iterator2) // 输出:iterator1 和 iterator2 相等 

外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。内部迭代
器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。

###迭代类数组对象和字面量对象
迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。比如 arguments、{"0":'a',"1":'b'}等。通过上面的代码可以观察到,无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。在 JavaScript 中,for in 语句可以用来迭代普通字面量对象的属性。jQuery 中提供了$.each`函数来封装各种迭代行为:

$.each = (obj, callback) => {
    let value,
        i = 0
    const length = obj.length
    const isArray = isArraylike(obj)
    if (isArray) { // 迭代类数组
        for (; i < length; i++) {
            value = callback.call(obj[i], i, obj[i])
            if (value === false) {
                break
            }
        }
    } else {
        for (i in obj) { // 迭代 object 对象
            value = callback.call(obj[i], i, obj[i])
            if (value === false) {
                break
            }
        }
    }
    return obj
}

###倒序迭代器
迭代器模式提供了循环访问一个聚合对象中每个元素的方法,但它没有规定我们以顺序、倒序还是中序来循环遍历聚合对象。下面我们实现一个倒序访问的迭代器:

const reverseEach = (ary, callback) => {
    for (const l = ary.length - 1; l >= 0; l--) {
        callback(l, ary[l])
    }
}
reverseEach([0, 1, 2], (i, n) => {
    console.log(n) // 分别输出:2, 1 ,0
})

中止迭代器

迭代器可以像普通 for 循环中的 break 一样,提供一种跳出循环的方法。

if (value === false) {
    break
}

约定如果回调函数的执行结果返回 false,则提前终止循环,终止迭代器示例如下:

const each = (ary, callback) => {
    for (let i = 0, l = ary.length; i < l; i++) {
        if (callback(i, ary[i]) === false) { // callback 的执行结果返回 false,提前终止迭代
            break
        }
    }
}
each([1, 2, 3, 4, 5], (i, n) => {
    if (n > 3) { // n 大于 3 的时候终止循环
        return false
    }
    console.log(n) // 分别输出:1, 2, 3
})

###迭代器模式的应用举例
【根据不同的浏览器获取相应的上传组件对象】
在不同的浏览器环境下,选择的上传方式是不一样的。因为使用浏览器的上传控件进行上传速度快,可以暂停和续传,所以我们首先会优先使用控件上传。如果浏览器没有安装上传控件,则使用 Flash 上传, 如果连 Flash 也没安装,那就只好使用浏览器原生的表单上传了。

const getUploadObj = () => {
    try {
        return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
    } catch (e) {
        if (supportFlash()) { // supportFlash 函数未提供
            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'))
        }
    }
}

以上代码虽然可以实现该需求,但是存在很明显的缺陷:

  • 可读性差
  • 违反开放封闭原则

后续还有其他上传方案的话只有继续添加if-else分支,比较难看。

下面我们使用迭代器模式来实现上面需求,代码如下:

// 定义各类上传方案的初始化类
const getActiveUploadObj = () => {
    try {
        return new ActiveXObject("TXFTNActiveX.FTNUpload") // IE 上传控件
    } catch (e) {
        return false
    }
}

const getFlashUploadObj = () => {
    if (supportFlash()) { // supportFlash 函数未提供
        const str = '<object type="application/x-shockwave-flash"></object>'
        return $(str).appendTo($('body'))
    }
    return false
}

const getFormUpladObj = () => {
    const str = '<input name="file" type="file" class="ui-file"/>' // 表单上传
    return $(str).appendTo($('body'))
}

// 迭代器使得定义的上传组件可以按照优先级进行循环迭代
// 迭代器内部循环过程假如返回一对象,则执行对应的方法,反之则返回false继续执行
const iteratorUploadObj = () => {
    for (let i = 0, fn; fn = arguments[i++];) {
        const uploadObj = fn()
        if (uploadObj !== false) {
            return uploadObj
        }
    }
}
const uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUpladObj)

很显然,迭代器模式中,功能职责的分配比较清晰,也比较容易理解,可以很方便地的维护和扩展。

总结

可以看出,合理运用迭代器模式可以使我们在开发过程很清楚地规划代码逻辑,使我们编写的代码拥有很好的维护性和拓展性,实现成本也比较低,还是比较简单,很容易理解的,更何况现在包括JavaScript在内的大部分语言都内置迭代器,我们只需遵循一定的规则去使用就好了。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions