迭代器模式
定义
迭代器模式是指提供一种方法顺序访问一个集合对象中的各个元素,而不需要暴露该对象的内部表示。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
主要解决
不需要关心对象内部表示来遍历整个对象。
内部迭代器
内部迭代器是指事先定义好迭代器的迭代规则,他完全接受整个迭代过程,外部只需要一次初始调用。
实现
我们实现一个each
内部迭代器,接受两个参数:
- arr:要被迭代的数组
- fn:迭代规则函数
fn在每一次数组循环时都会执行
1 | const each = function(arr, fn){ |
内部迭代器的缺点就是无法很好的组合多个迭代,只有在一个迭代规则中嵌入另外一个迭代器,这样未必显得代码冗余,影响代码可读性。所以产生了外部迭代器。
外部迭代器
外部迭代器一般会提供一个next
类似的函数,每调用一次,就会返回该次迭代的结果。我们可以在外部拿到结果,进行更多的操作。
下面我们简单实现一个外部迭代器的原型:
1 | const Iterator = function(obj){ |
上面的函数实现了ES6提供的Iterator
接口最基本的部分。我们可以在外部拿到迭代结果,现在我们可以同时拿到两个迭代器的结果而不用嵌套迭代器。
见到那写一下两个迭代器元素的比较:
1 | let compare = function(iterator1, iterator2){ |
JavaScript中遍历
迭代器说到底还是对某种数据结构进行遍历的的一个接口。在一般的高级语言中,对于特殊的数据结构,比如数组都会封装一个方法进行遍历。而在JavaScript中,表示集合类的数据结构中,包括ES6增加的Map
和Set
,一共就有4种:
Object
Array
Set
Map
对于数组和对象,我在JavaScript中数组与对象的遍历方法中,进行了详细的探讨。
在那篇文章中,实际上都是数组或对象的内部迭代器的实例,比如的数组的forEach
等方法。
而在ES6中,提供了原生的Iterator
接口来实现外部迭代器,而在Java等语言中,早就提供了该接口。
Iterator
Iterator
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of
循环(详见下文)。当使用for...of
循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 规定,
- 默认的 Iterator 接口部署在数据结构的
Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。 Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。- 属性名
Symbol.iterator
,它是一个表达式,返回Symbol
对象的iterator
属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
如:
1 | const obj = { |
上面代码中,对象obj
是可遍历的(iterable),因为具有Symbol.iterator
属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next
方法。每次调用next
方法,都会返回一个代表当前成员的信息对象,具有value
和done
两个属性。
ES6中有一些对象已经原生实现了Iterator 接口:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
值得注意的是Object
并没有原生实现iterator
,原因是:
对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
当我们对一个数组完成赋值,其Symbol.iterator
属性就随之生成了。比如下面的例子:
1 | let arr = ['a', 'b', 'c'] |
调用 Iterator 接口的场合
有一些场合会默认调用 Iterator 接口(即Symbol.iterator
方法),除了下文会介绍的for...of
循环,还有几个别的场合。
(1)解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator
方法。
1 | let set = new Set().add('a').add('b').add('c'); |
(2)扩展运算符
扩展运算符(…)也会调用默认的 Iterator 接口。
1 | // 例一 |
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
1 | let arr = [...iterable]; |
(3)yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
1 | let generator = function* () { |
(4)其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
Iterator 接口与 Generator 函数
Symbol.iterator()
方法的最简单实现,还是使用Generator 函数。
1 | let myIterable = { |
上面代码中,Symbol.iterator()
方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
for…of 循环
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of
循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator
属性,就被视为具有 iterator 接口,就可以用for...of
循环遍历它的成员。也就是说,for...of
循环内部调用的是数据结构的Symbol.iterator
方法。
for...of
循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments
对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
示例
1 | //数组 |
计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
entries()
返回一个遍历器对象,用来遍历[键名, 键值]
组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries
方法。keys()
返回一个遍历器对象,用来遍历所有的键名。values()
返回一个遍历器对象,用来遍历所有的键值。
参考
- 《JavaScript设计模式与开发实践》
- Iterator 和 for…of 循环