迭代是编程中的一种常见做法,通常用于循环一组值,或者转换每个值,或者以某种方式使用或保存它以供以后使用。
在 JavaScript 中,我们经常使用类似以下的 for
循环:
for (var i = 0; i < foo.length; i++) {
// ...
}
但 ES6 为我们提供了另一种选择:
for (const i of foo) {
// ...
}
这无疑是一种更简洁、更易于使用的方式。但关于这种新的迭代,还有一点非常重要:它允许您直接与数据集的元素交互。
假设我们要找到数组中大于 20
的元素放置到另一个数组中:
const arr = [73, 6, 90, 19, 15]
const newArr = []
for (var i = 0; i < arr.length; i++) {
if (arr[i] > 20) {
newArr.push(arr[i])
}
}
newArr // [73, 90]
它可以工作,但它很笨重,而且这种笨重很大程度上取决于 JavaScript 处理 for
循环的方式。然而,对于 ES6,我们在新的迭代器中可以这样写:
const arr = [73, 6, 90, 19, 15]
const newArr = []
for (const i of arr) {
if (i > 20) {
newArr.push(i)
}
}
newArr // [73, 90]
这要干净得多,但最引人注目的是 for
循环。变量 i
现在表示数组中名为 newArr
的实际项。所以,我们不再需要通过索引来调用它。这意味着,我们可以直接调用 i
,而不是调用循环中的 newArr[i]
。
在这背后,这种迭代利用了 ES6 的 Symbol.iterator() 方法。它负责描述迭代,并在被调用时返回一个 JavaScript 对象,其中包含循环中的 next
值和一个 done
键,该键是 true
还是 false
取决于循环是否完成。
以下是一个数组:
const numbers = [1, 2, 3]
numbers[Symbol.iterator] // function values() { [native code] }
ES6 规范定义了对象的非字符串 symbol
属性名称,以描述特定行为。Symbol.iterator
是其中之一,它描述了迭代是如何工作的。
const numbersIterator = numbers[Symbol.iterator]()
numbersIterator.next() // { value: 1, done: false }
numbersIterator.next() // { value: 2, done: false }
numbersIterator.next() // { value: 3, done: false }
numbersIterator.next() // { value: undefined, done: true }
以上是 for (var num of numbers)
在 JS 引擎下所做的事情。
当我们调用 numbers[Symbol.iterator]
时,会返回一个带有 next
方法的对象。调用 next
会给我们一个包含 value
的对象,或者表示没有其他值了。
让我们创建一个对象来迭代字符串中的 Words
(以一种非常简单的方式)。
首先,创建一个构造函数:
function Words(str) {
this._str = str
}
然后,创建一个迭代器工厂:
Words.prototype[Symbol.iterator] = function () {
const re = /\S+/g
const str = this._str
return {
next() {
const match = re.exec(str)
if (match) {
return { value: match[0], done: false }
}
return { value: undefined, done: true }
}
}
}
我们返回一个带有 next
方法的对象,它返回 { value: nextWordInTheString, done: false }
,直到没有 next
的方法。
const helloWorld = new Words('Hello world')
for (var word of helloWorld) {
console.log(word)
}
// 'Hello'
// 'world'
它可以正常工作!
Generator 译为生成器,也称为“迭代器工厂”,是 ES6 JavaScript 函数的一个补充,用于创建特定的迭代。它们为你提供了特殊的、自定义的循环方法。
它是一种特殊的函数,能够暂停自身,稍后再恢复,同时允许其他代码运行。
代码决定它必须等待,因此它允许“队列中”的其他代码运行,并保留在“等待的事情”完成时恢复其操作的权利。
所有这些都是用一个简单的关键字完成的:yield
。当生成器包含该关键字时,执行将暂停。
生成器可以包含许多 yield
关键字,因此会多次停止,它由 function *
关键字标识。
生成器支持 JavaScript 编程的全新范例,允许:
- 生成器运行时的双向通信
- 不会冻结程序的长寿命
while
循环
使用 function*
定义的生成器是创建迭代器工厂的一种更方便的方式。
从生成器中,你 yield
你想要提供的值,它会为你处理 value/done
对象:
function* someNumbers() {
yield 1
yield 2
yield 3
}
// 初始化
const iter = someNumbers()
iter.next() // { value: 1, done: false }
iter.next() // { value: 2, done: false }
iter.next() // { value: 3, done: false }
iter.next() // { value: undefined, done: true }
这一切都发生在您使用 yield
关键字暂停代码的运行,以及是否调用 next
方法再次启动迭代器。
我们可以使用 for...of
循环它:
for (var num of someNumbers()) {
console.log(num)
}
// 1
// 2
// 3
尽管上面的 for...of
循环看起来很直观,但它只是通过一点小技巧才能工作。for...of
调用对象的 Symbol.iterator
方法来获得迭代器,但我们给出的 someNumbers()
已经是一个迭代器了。为了解决这个问题,生成器返回的迭代器有一个 Symbol.iterator
方法返回自身,表示 iter[Symbol.iterator]() === iter
。这有点奇怪,但它使上述代码按预期工作。
iter[Symbol.iterator]() // Object [Generator] {}
iter[Symbol.iterator]() === iter // true
生成器通常是描述迭代的一种更简单的方法。对于我们的上述的 Words
示例,它要简单得多:
Words.prototype[Symbol.iterator] = function* () {
const re = /\S+/g
const str = this._str
let match
while ((match = re.exec(str))) {
yield match[0]
}
}
注意:箭头函数不能作为 Generator 函数,不能使用
yield
关键字。
另外,当值以异步方式出现时,将使用 Symbol.asyncIterator
替代 Symbol.iterator
。
本文更多详细的内容可以阅读下文提供的资料。