场景

for...of.... 的原理是?

迭代器模式

看下 维基百科 给的定义:

In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled.

说白了就是有个容器类,有一个迭代器类,容器类持有一个迭代器类的对象,然后我们不需要知道容器中元素的具体结构,通过迭代器对象就能够进行遍历。

image-20220226101825545

不妨可以看下 java 的具体实现:

1public interface Iterator<E> {
2  boolean hasNext();
3  void next();
4  E currentItem();
5}
6
7// 迭代器类
8public class ArrayIterator<E> implements Iterator<E> {
9  private int cursor;
10  private ArrayList<E> arrayList;
11
12  public ArrayIterator(ArrayList<E> arrayList) {
13    this.cursor = 0;
14    this.arrayList = arrayList;
15  }
16
17  @Override
18  public boolean hasNext() {
19    return cursor != arrayList.size();
20  }
21
22  @Override
23  public void next() {
24    cursor++;
25  }
26
27  @Override
28  public E currentItem() {
29    if (cursor >= arrayList.size()) {
30      throw new NoSuchElementException();
31    }
32    return arrayList.get(cursor);
33  }
34}
35
36public class Demo {
37  public static void main(String[] args) {
38    ArrayList<String> names = new ArrayList<>();
39    names.add("wind");
40    names.add("liang");
41    names.add("2022");
42
43    Iterator<String> iterator = new ArrayIterator(names);
44    while (iterator.hasNext()) {
45      System.out.println(iterator.currentItem());
46      iterator.next();
47    }
48  }
49}

容器类使用 java 自带的 ArrayList 类,然后我们手动实现一个迭代器类 ArrayIterator

js 的迭代器模式

js 中我们不需要专门定义迭代器的类了,我们可以让容器包含一个 Symbol.iterator 方法,该方法返回一个迭代器对象。

迭代器对象包含一个 next 方法用来获取元素,同时获取到的元素除了本身的 value 外,还返回一个布尔型变量代表是否有下一个元素。

1function container(arr) {
2  let nextIndex = 0
3  return {
4    [Symbol.iterator]() {
5      return {
6        next() {
7          return nextIndex < arr.length
8            ? {
9                value: arr[nextIndex++],
10                done: false,
11              }
12            : {
13                value: undefined,
14                done: true,
15              }
16        },
17      }
18    },
19  }
20}
21
22const list = container(['wind', 'liang', '亮'])
23const iterator = list[Symbol.iterator]()
24
25while (true) {
26  const data = iterator.next()
27  if (data.done)
28    break
29  else
30    console.log(data.value)
31}

事实上,数组已经为我们提前实现了迭代器,我们直接通过 Symbol.iterator 方法拿到,不需要自己再实现了。

1const array = ['wind', 'liang', '亮']
2const iteratorArray = array[Symbol.iterator]()
3
4while (true) {
5  const data = iteratorArray.next()
6  if (data.done)
7    break
8  else
9    console.log(data.value)
10}

还有字符串也为我们内置了迭代器。

1const string = 'windliang'
2const iteratorString = string[Symbol.iterator]()
3
4while (true) {
5  const data = iteratorString.next()
6  if (data.done)
7    break
8  else
9    console.log(data.value)
10}

同理,MapSet 都帮我们内置了 Symbol.iterator 方法,可以返回一个迭代器。

此外,我们也不需要每次都去 while 循环、然后判断是否结束循环了,直接使用 for...of... 即可。

1const array = ['wind', 'liang', '亮']
2for (const a of array)
3  console.log(a)
4
5const string = 'windliang'
6for (const s of string)
7  console.log(s)

注意

因为数组是通过 index 来获取元素的,如果在遍历过程中删除元素,可能会产生非预期内的事情。

1const array = ['wind', 'liang', '亮']
2for (const a of array) {
3  console.log(a)
4  if (a === 'wind')
5    array.splice(0, 1)
6}
7console.log(array)

可以先思考下会怎么输出,然后看下结果:

1wind
2[('liang', '亮')]

我们是成功删除了 wind ,但是原数组中 liang 就不会遍历到了,也比较好理解。

开始的时候,指针 index 指向 wind,进行了输出 console.log(a); // wind

10     1   2
2wind liang 亮
3 ^
4index

此时删除了 windarray.splice(0, 1); 数组整体前移。

10    1
2liang 亮
3 ^
4index

然后指针后移,遍历下个元素。

10     1
2liang 亮
3      ^
4     index

就直接走到 了,而没有遍历 liang

原因就是 liang 的位置之前是 windwind 之前已经遍历过了,指针后移就把 liang 跳过了。

迭代器模式的好处就是可以不知道容器中元素的结构就可以遍历,一般由容器提供一个迭代器供我们使用。为了实现不同的遍历顺序,只需要提供新的迭代器即可。

一般编程语言中都内置了迭代器,js 也不例外,在 ArrayStringMapSet 中都内置了Symbol.iterator 方法返回一个迭代器对象,同时提供了for...of... 语法统一了各个对象的遍历方式。