Skip to main content

第七章 迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

目前,恐怕只有在一些“古董级”的语言中才会为实现一个迭代器模式而烦恼。 现在流行的大部分语言如 Java、Ruby 等都已经有了内置的迭代器实现,许多浏览器也支持 JavaScript 的 Array.prototype.forEach

7.1 jQuery 中的迭代器

迭代器模式无非就是循环访问聚合对象中的各个元素。

比如 jQuery 中的 $.each 函数,其中回调函数中的参数 i 为当前索引,n 为当前元素。

7.2 实现自己的迭代器

现在我们来自己实现一个 each 函数,each 函数接受 2 个参数,

  1. 第一个为被循环的数组,

  2. 第二个为循环中的每一步后将被触发的回调函数。

var each = function (arr, callback) {
for (let i = 0; i < arr.length; i++) {
// 把元素和下标当作参数传给 callback
callback.apply(arr[i], [arr[i], i]);
}
};

// TEST
let arr1 = [1, 2, 3, 4, 5];
each(arr1, (item, ind) => {
console.log(item, ind);
});

7.3 内部迭代器和外部迭代器

迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景。

内部迭代器

刚编写的 each 函数属于内部迭代器,each 函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。

内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,但这也刚好是内部迭代器的缺点。

比如现在有个需求,要判断 2 个数组里元素的值是否完全相等, 如果不改写 each 函数本身的代码,我们能够入手的地方似乎只剩下 each 的回调函数了。

let arr1 = [1, 2, 3, 4, 5];
let arr2 = [1, 2, 3, 4, 5];
let arr3 = [5, 4, 3, 2, 1];

var each = function (arr, callback) {
for (let i = 0; i < arr.length; i++) {
// 把元素和下标当作参数传给 callback
callback.apply(arr[i], [arr[i], i]);
}
};

var compare = function (arr1, arr2) {
function throwError() {
throw new Error("Failed.");
}

if (arr1.length !== arr2.length) {
throwError();
}

each(arr1, (item, ind) => {
if (item !== arr2[ind]) {
throwError();
}
});

return true;
};

console.log(compare(arr1, arr2));
console.log(compare(arr1, arr3));

但这个 compare 函数一点都算不上好看。

在一些没有闭包的语言中,内部迭代器本身的实现也相当复杂。

比如 C 语言中的内部迭代器是用 函数指针 来实现的,循环处理所需要的数据都要以参数的形式明确地从外面传递进去。

外部迭代器

外部迭代器必须显式地请求迭代下一个元素。

外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。

下面这个外部迭代器的实现来自《松本行弘的程序世界》第 4 章,原例用 Ruby 写成,这里我们翻译成 JavaScript:

var Iterator = function (obj) {
var current = 0;
var next = function () {
current += 1;
};
var isDone = function () {
return current >= obj.length;
};
var getCurrItem = function () {
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem,
};
};

再看看如何改写 compare 函数:

var compare = function (iterator1, iterator2) {
while (!iterator1.isDone() && !iterator2.isDone()) {
if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
throw new Error("iterator1 和 iterator2 不相等");
}
iterator1.next();
iterator2.next();
}
alert("iterator1 和 iterator2 相等");
};

var iterator1 = Iterator([1, 2, 3]);
var iterator2 = Iterator([1, 2, 3]);
compare(iterator1, iterator2); // 输出:iterator1 和 iterator2 相等

外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。

内部迭代器和外部迭代器在实际生产中没有优劣之分,究竟使用哪个要根据需求场景而定。

7.4 迭代类数组对象和字面量对象

迭代器模式不仅可以迭代数组,还可以迭代一些类数组的对象。

比如 arguments{"0": 'a', "1": 'b'} 等。

通过自己实现迭代器可以观察到,无论是内部还是外部迭代器,只要被迭代的聚合对象拥有 length 属性且可以用下标访问,那它就可以被迭代。

在 JavaScript 中,for in 语句可以用来迭代普通字面量对象的属性。

字面量对象 Object Literal就是空对象 {},或被 {} 包含且其中含有键-值对的逗号分隔列表。

是直接在源代码中定义数据结构的方法,而无需调用特定的构造函数或方法。 字面量对象提供了一种简洁、直观的方式来表示复杂的数据结构。

7.5 倒序迭代器

由于 GoF 中对迭代器的定义非常松散,所以我们可以有各种各样的迭代器实现。

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

现在我们来实现一个倒序的迭代器:

var reverseEach = function (arr, callback) {
for (let i = arr.length - 1; i >= 0; i--) {
callback.apply(arr[i], [arr[i], i]);
}
};

let arr1 = [1, 2, 3, 4, 5];
reverseEach(arr1, (item, ind) => {
console.log(item); // 5 4 3 2 1
});

7.6 中止迭代器

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

如果回调函数的执行结果返回 false,则提前终止循环。

现在来改造一下 each 函数。

var each = function (arr, callback) {
for (let i = 0; i < arr.length; i++) {
let r = callback.apply(arr[i], [arr[i], i]);
if (r === false) {
break;
}
}
};

// TEST
each([1, 2, 3, 4, 5], (item, ind) => {
if (item > 3) {
return false;
}
console.log(item); // 1 2 3
});

7.7 迭代器的应用举例

下面这段代码,它的目的是根据不同的浏览器获取相应的上传组件对象:

var getUploadObj = function () {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
} catch (e) {
if (supportFlash()) {
// supportFlash 函数未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $(str).appendTo($("body"));
} else {
var str = '<input name="file" type="file"/>'; // 表单上传
return $(str).appendTo($("body"));
}
}
};

在不同的浏览器环境下,选择的上传方式是不一样的。

因为使用浏览器的上传控件进行上传速度快,可以暂停和续传,所以我们首先会优先使用控件上传。

如果浏览器没有安装上传控件,则使用 Flash 上传。

如果连 Flash 也没安装,那就只好使用浏览器原生的表单上传了。

上面的代码,为了得到一个 upload 对象,这个 getUploadObj 函数里面充斥了 trycatch 以及 if 条件分支。

这样的代码

  1. 很难阅读,

  2. 严重违反开闭原则。

    在开发和调试过程中,我们需要来回切换不同的上传方式,每次改动都相当痛苦。

    后来我们还增加支持了一些另外的上传方式,比如,HTML5 上传,这时候唯一的办法是继续往 getUploadObj 函数里增加条件分支。

现在来梳理一下问题,目前一共有 3 种可能的上传方式,我们不知道目前正在使用的浏览器支持哪几种。

就好比我们有一个钥匙串,其中共有 3 把钥匙,我们想打开一扇门但是不知道该使用哪把钥匙,于是从第一把钥匙开始,迭代钥匙串进行尝试,直到找到了正确的钥匙为止。

var getActiveUploadObj = function () {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
} catch (e) {
return false;
}
};

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

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

getActiveUploadObjgetFlashUploadObjgetFormUpladObj3 个函数中都有同一个约定:

如果该函数里面的 upload 对象是可用的,则让函数返回该对象,反之返回 false,提示迭代器继续往后面进行迭代。

var iteratorUploadObj = function () {
for (let i = 0, fn; (fn = arguments[i++]); ) {
let uploadObj = fn;
if (!uploadObj) {
return uploadObj;
}
}
};

var uploadObj = iteratorUploadObj(
getActiveUploadObj,
getFlashUploadObj,
getFormUpladObj
);

重构代码之后,我们可以看到,获取不同上传对象的方法被隔离在各自的函数里互不干扰,trycatchif 分支不再纠缠在一起,使得我们可以很方便地的维护和扩展代码。

比如,后来我们又给上传项目增加了 Webkit 控件上传和 HTML5 上传,我们要做的仅仅是下面一些工作。

var getWebkitUploadObj = function () {
// 具体代码略
};

var getHtml5UploadObj = function () {
// 具体代码略
};

// 按照优先级将它们添加进入迭代器
var uploadObj = iteratorUploadObj(
getActiveUploadObj,
getWebkitUploadObj,
getFlashUploadObj,
getHtml5UploadObj,
getFormUpladObj
);