JavaScript迭代器详解:从协议到实践

本文深入解析JavaScript中的迭代器与可迭代对象,详细讲解迭代协议、Symbol.iterator方法、next()方法的工作原理,并通过实际代码示例展示如何创建和使用迭代器。

JavaScript迭代器详解:从协议到实践

迭代器是JavaScript中语言上比较令人困惑的主题之一,轻松越过了本已相当高的理解门槛。有可迭代对象——数组、Set、Map和字符串——它们都遵循可迭代协议。要遵循该协议,对象必须实现可迭代接口。实际上,这意味着对象需要在其原型链中的某处包含一个[Symbol.iterator]()方法。

可迭代协议是两个迭代协议之一。另一个迭代协议是迭代器协议。

明白我说的语言上很棘手是什么意思了吗?可迭代对象实现可迭代迭代接口,而迭代器实现迭代器迭代接口!如果你能快速说五遍,那么你就基本掌握了要点;简单易行,对吧?

不,听着,我保证当你学完这课时,它不会像听起来那么令人困惑,特别是你已经有了前面课程的背景知识。

可迭代对象与迭代器

可迭代对象遵循可迭代协议,这仅仅意味着该对象有一个用于创建迭代器的常规方法。它包含的元素可以使用for...of循环遍历。

迭代器对象遵循迭代器协议,它包含的元素可以按顺序、一次一个地访问。

再次强调——我为自己玩这个文字游戏不请求原谅,也不期望你原谅我——迭代器对象遵循迭代器协议,它包含的元素可以按顺序、一次一个地访问。迭代器协议定义了一种产生值序列的标准方式,并在所有可能的值生成后可选地返回一个值。

为了遵循迭代器协议,一个对象必须——你猜对了——实现迭代器接口。实际上,这再次意味着某个方法必须在对象的原型链上的某处可用。在这种情况下,是next()方法,它一次一个地遍历其包含的元素,并在每次调用该方法时返回一个对象。

为了满足迭代器接口标准,返回的对象必须包含两个具有特定键的属性:一个键为value,表示当前元素的值;另一个键为done,一个布尔值,告诉我们迭代器是否已经前进到超过数据结构中最后一个元素的位置。这不是编辑团队疏忽让通过的尴尬措辞:只有当调用next()导致尝试访问迭代器中最后一个元素之后的元素时,该done属性的值才为true,而不是在访问迭代器中最后一个元素时。再说一次,文字描述很多,但当你看到实际操作时,它会更有意义。

内置迭代器示例

你以前见过内置迭代器的例子,尽管很简短:

1
2
3
4
const theMap = new Map([ [ "aKey", "A value." ] ]);

console.log( theMap.keys() );
// 结果:Map Iterator { constructor: Iterator() }

没错:虽然Map对象本身是一个可迭代对象,但Map的内置方法keys()values()entries()都返回迭代器对象。你还会记得我使用forEach(这是语言中相对较新的添加)循环遍历了它们。以这种方式使用,迭代器与可迭代对象没有区别:

1
2
3
4
5
6
const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys().forEach( thing => {
  console.log( thing );
});
// 结果:key

所有迭代器都是可迭代的;它们都实现了可迭代接口:

1
2
3
4
const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys()[ Symbol.iterator ];
// 结果:function Symbol.iterator()

创建迭代器

如果你对迭代器和可迭代对象之间界限日益模糊感到愤怒,等到你看到这个"十大动漫背叛"视频候选者时再说:我将演示如何使用数组与迭代器交互。

“嘘,“你肯定会喊,被你最古老、最常索引的朋友之一如此背叛。“数组是可迭代对象,不是迭代器!“你对我大喊大叫在总体上是对的,对数组的具体看法也是对的——数组是可迭代对象,不是迭代器。事实上,虽然所有迭代器都是可迭代的,但没有任何内置的可迭代对象是迭代器。

然而,当你调用那个[Symbol.iterator]()方法——定义对象为可迭代对象的方法——它会从可迭代数据结构返回一个迭代器对象:

1
2
3
4
5
6
7
8
const theIterable = [ true, false ];
const theIterator = theIterable[ Symbol.iterator ]();

theIterable;
// 结果:Array [ true, false ]

theIterator;
// 结果:Array Iterator { constructor: Iterator() }

Set、Map和——是的——甚至字符串也是如此:

1
2
3
4
5
const theIterable = "A string."
const theIterator = theIterable[ Symbol.iterator ]();

theIterator;
// 结果:String Iterator { constructor: Iterator() }

我们在这里手动做的事情——使用%Symbol.iterator%从可迭代对象创建迭代器——正是可迭代对象内部的工作方式,也是为什么它们必须实现%Symbol.iterator%才能成为可迭代对象的原因。任何时候你循环遍历数组,你实际上是在循环遍历从该数组创建的迭代器。所有内置迭代器都是可迭代的。所有内置可迭代对象都可以用来创建迭代器。

或者——甚至更可取,因为它不需要你直接接触%Symbol.iterator%——你可以使用内置的Iterator.from()方法从任何可迭代对象创建迭代器对象:

1
2
3
4
const theIterator = Iterator.from([ true, false ]);

theIterator;
// 结果:Array Iterator { constructor: Iterator() }

使用next()方法遍历

你还记得我提到过迭代器必须提供next()方法(返回一个非常特定的对象)吗?调用该next()方法一次一个地逐步遍历迭代器包含的元素,每次调用返回该对象的一个实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const theIterator = Iterator.from([ 1, 2, 3 ]);

theIterator.next();
// 结果:Object { value: 1, done: false }

theIterator.next();
// 结果:Object { value: 2, done: false }

theIterator.next();
// 结果:Object { value: 3, done: false }

theIterator.next();
// 结果:Object { value: undefined, done: true }

你可以将其视为比你可能习惯的传统"上发条然后看它走"的for循环更受控的遍历形式——一种按需一次一步访问元素的方法。当然,你不必以这种方式逐步遍历迭代器,因为它们有自己专门的Iterator.forEach方法,其工作方式完全如你所期望——到某一点:

1
2
3
4
5
6
7
const theIterator = Iterator.from([ true, false ]);

theIterator.forEach( element => console.log( element ) );
/* 结果:
true
false
*/

关键区别:状态维护

但我们还没有触及可迭代对象和迭代器之间的另一个重大区别,在我看来,它实际上在很大程度上帮助我们从语言上理解这两者。不过,你可能需要在这里稍微迁就我一下。

看,可迭代对象是一个可迭代的对象。不,听着,跟我来:你可以迭代一个数组,当你完成后,你仍然可以迭代那个数组。根据定义,它是一个可以被迭代的对象;可迭代的本质就是可迭代的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const theIterable = [ 1, 2 ];

theIterable.forEach( el => {
  console.log( el );
});
/* 结果:
1
2
*/

theIterable.forEach( el => {
  console.log( el );
});
/* 结果:
1
2
*/

在某种程度上,迭代器对象代表了迭代的单一行为。在可迭代对象内部,它是每次执行迭代时,可迭代对象被迭代的机制。作为一个独立的迭代器对象——无论你是使用next方法逐步遍历它,还是使用forEach循环遍历它的元素——一旦被迭代,那个迭代器就是过去式;它已经被迭代了。因为它们维护内部状态,迭代器的本质是被迭代,单数的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const theIterator = Iterator.from([ 1, 2 ]);

theIterator.next();
// 结果:Object { value: 1, done: false }

theIterator.next();
// 结果:Object { value: 2, done: false }

theIterator.next();
// 结果:Object { value: undefined, done: true }

theIterator.forEach( el => console.log( el ) );
// 结果:undefined

迭代器的实用方法

当你使用迭代器构造器的内置方法来,比如说,过滤或提取迭代器对象的一部分时,这可以做出整洁的工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const theIterator = Iterator.from([ "First", "Second", "Third" ]);

// 从`theIterator`中取前两个值:
theIterator.take( 2 ).forEach( el => {
  console.log( el );
});
/* 结果:
"First"
"Second"
*/

// theIterator现在只包含上述操作完成后剩下的任何内容:
theIterator.next();
// 结果:Object { value: "Third", done: false }

一旦你到达迭代器的末尾,迭代它的行为就完成了。已迭代。过去式。

你可能也 relieved 地听到,你在这节课的时间也是如此。我知道这有点难,但好消息是:这门课程是可迭代的,不是迭代器。你在其中迭代的这一步——这节课——可能结束了,但这门课程的本质是你可以再次迭代它。不用担心现在就把所有这些都记下来——你可以随时回来重温这节课。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计