JavaScript设计模式(五):迭代器模式

迭代器模式

文章内容分两部分:

  1. 前半部分为 “迭代器模式” 概念;
  2. 后半部分为 ES6 中 Iterator (迭代器)

上半部分开始…

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

简单理解(白话理解):统一 “集合” 型数据结构的遍历接口,实现可循环遍历获取集合中各数据项(不关心数据项中的数据结构)。

生活小栗子:清单 TodoList。每日清单有学习类、生活类、工作类、运动类等项目,清单列表只管罗列,不管类别。

模式特点

  1. 为遍历不同数据结构的 “集合” 提供统一的接口;
  2. 能遍历访问 “集合” 数据中的项,不关心项的数据结构

模式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 统一遍历接口实现
var each = function(arr, callBack) {
for (let i = 0, len = arr.length; i < len; i++) {
// 将值,索引返回给回调函数callBack处理
if (callBack(i, arr[i]) === false) {
break; // 中止迭代器,跳出循环
}
}
}
// 外部调用
each([1, 2, 3, 4, 5], function(index, value) {
if (value > 3) {
return false; // 返回false中止each
}
console.log([index, value]);
})
// 输出:[0, 1] [1, 2] [2, 3]

“迭代器模式的核心,就是实现统一遍历接口。”

模式细分

  1. 内部迭代器 (jQuery 的 $.each / for…of)
  2. 外部迭代器 (ES6 的 yield)

内部迭代器

内部迭代器: 内部定义迭代规则,控制整个迭代过程,外部只需一次初始调用

1
2
3
4
5
6
// jQuery 的 $.each(跟上文each函数实现原理类似)
$.each(['Angular', 'React', 'Vue'], function(index, value) {
console.log([index, value]);
});
// 输出:[0, Angular] [1, React] [2, Vue]

优点:调用方式简单,外部仅需一次调用
缺点:迭代规则预先设置,欠缺灵活性。无法实现复杂遍历需求(如: 同时迭代比对两个数组)

外部迭代器

外部迭代器: 外部显示(手动)地控制迭代下一个数据项

借助 ES6 新增的 Generator 函数中的 yield* 表达式来实现外部迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES6 的 yield 实现外部迭代器
function* generatorEach(arr) {
for (let [index, value] of arr.entries()) {
yield console.log([index, value]);
}
}
let each = generatorEach(['Angular', 'React', 'Vue']);
each.next();
each.next();
each.next();
// 输出:[0, 'Angular'] [1, 'React'] [2, 'Vue']

优点:灵活性更佳,适用面广,能应对更加复杂的迭代需求
缺点:需显示调用迭代进行(手动控制迭代过程),外部调用方式较复杂

适用场景

不同数据结构类型的 “数据集合”,需要对外提供统一的遍历接口,而又不暴露或修改内部结构时,可应用迭代器模式实现。


下半部分开始…

ES6 的 Iterator 迭代器

“迭代器等同于遍历器。在某些文章中,可能会出现遍历器的字眼,其实两者的意思一致。”

JavaScript 中 原有表示 “集合” 的数据结构主要是 “数组(Array)” 和 “对象(Object)”,ES6又新增了 MapSet,共四种数据集合,浏览器端还有 NodeList 类数组结构。为 “集合” 型数据寻求统一的遍历接口,正是 ES6 的 Iterator 诞生的背景。

ES6 中迭代器 Iterator 作为一个接口,作用就是为各种不同数据结构提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作。

Iterator 作用:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列;
  3. 为新的遍历语法 for...of 实现循环遍历

Iterator只是一种接口,与遍历的数据结构是分开的。 重温迭代器模式特点:我只要统一遍历数据项的接口,不关心其数据结构。

ES6 默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性上,该属性本身是一个函数,代表当前数据结构默认的遍历器生成函数。执行该函数 [Symbol.iterator](),会返回一个遍历器对象。只要数据结构拥有 Symbol.iterator 属性,那么它就是 “可遍历的” 。

遍历器对象的特征:

  1. 拥有 next 属性方法;
  2. 执行 next(),会返回一个包含 valuedone 属性的对象
    • value: 当前数据结构成员的值
    • done: 布尔值,表示遍历是否结束

原生具备 Iterator 接口的数据结构:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. 函数的 arguments 对象
  7. NodeList 对象
1
2
3
4
5
6
7
let arr = ['a', 'b', 'c'];
let iterator = arr[Symbol.iterator]();
iterator.next(); // { value: 'a', done: false }
iterator.next(); // { value: 'b', done: false }
iterator.next(); // { value: 'c', done: false }
iterator.next(); // { value: undefined, done: false }

原生部署 Iterator 接口的数据结构,无需手动执行遍历器生成函数,可使用 for...of 自动循环遍历。

for...of 运行原理:

  1. 首先调用遍历对象 [Symobo.iterator]() 方法,拿到遍历器对象;
  2. 每次循环,调用遍历器对象 next() 方法,得到 {value: ..., done: ... } 对象
1
2
3
4
5
6
7
// for...of 自动遍历拥有 Iterator 接口的数据结构
let arr = ['a', 'b', 'c'];
for (let item of arr) {
console.log(item);
}
// 输出:a b c

类数组对象:存在数值键名和 length 属性的对象

类数组对象部署 Iterator 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方法一:
NodeList.prototype[Symbol.iterator] = Array.prototype[Sybmol.iterator];
// 方法二:
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
// for...of 遍历类数组对象
let arrLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of arrLike) {
console.log(item);
}
// 输出:a b c

对象(Object)没有默认 Iterator 接口,因为对象属性遍历顺序不确定,需开发者手动指定。

注意:

  1. 普通对象部署数组的 Symbol.iterator 方法,并无效果;
  2. 普通对象若 Symbol.iterator 方法对应的部署遍历器生成函数(即返回一个遍历器对象),解释引擎会报错。
1
2
3
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj]; // TypeError: [] is not a function

for...of 遍历普通对象的解决方法:

  1. 使用 Objet.keys 将对象键名生成一个数组,然后遍历该数组;
  2. Generator 函数重新包装对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let person = {
name: 'Ken',
sex: 'Male'
}
// Object.keys
for (let key of Object.keys(person)) {
console.log(`${key}: ${person[key]}`);
}
// Generator 包装对象
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(person)) {
console.log(`${key}: ${value}`);
}
// 输出:
// name: Ken
// sex: Male

ES6 的 Iterator 应用场景

  1. 解构赋值
  2. 扩展运算符
  3. yield*
  4. 任何以数组为参数的遍历的场景:
    • for...of
    • Array.from()
    • Map()/Set()/WeakMap()/WeakSet()
    • Promise.all()/Promise.race()

for…of 对比 for / for…in / forEach

for 循环 :需定义索引变量,指定循环终结条件。

1
2
3
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i]);
}

forEach: 无法中途跳出循环,break/return

1
2
3
forEach(arr, function(item, index) {
console.log(item, index);
})

for…in:

  1. 只能获取键名,不能获取键值
  2. 以字符串为键名(但数组的键名为数值类型索引)
  3. 任意顺序遍历键名(???)
  4. 会遍历手动添加的其它键(原型链上的键)
  5. 为遍历对象设计,不适用数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let triangle = {a: 1, b: 2, c: 3};
function ColoredTriangle() {
this.color = 'red';
}
ColoredTriangle.prototype = triangle;
let obj = new ColoredTriangle();
for (let prop in obj) {
// 需手动判断是否属于自身属性,而不是原型链属性
if (obj.hasOwnProperty(prop)) {
console.log(`obj.${prop} = ${obj[prop]}`);
}
}
// 输出:obj.color = red

for…of 较其它三者优点:

  1. for...in 一样简洁,但没有 for...in 的缺点;
  2. 不同于 forEach, 可使用 break/return/continue 退出循环;
  3. 提供了遍历所有数据的统一接口

缺点:遍历普通对象时,不能直接使用。


参考文章

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。

坚持原创技术分享,您的支持将鼓励我创作出的更好文章分享!