JavaScript 提供了许多迭代集合的方法,从简单的 for
循环到 forEach
、 map
和 filter
。其中 while
、do...while
、for
、for...in
、for...of
,可以被 label
标识,可以被 break
或者 continue
来指出程序是否该停止循环还是继续循环。
while
、do...while
、for
能够循环数组,如果要迭代对象则需要结合 Object.getOwnPropertyNames
(不包括 symbol
类型)。for...in
能够循环对象(包括数组),但是它会循环对象的所有可枚举属性,包括原型链上的属性。因此,通常需要结合 hasOwnProperty
对象实例方法来过滤。
ECMAScript 2015 引入 Object.keys
返回对象 key
的数组。ECMAScript 2017 引入 Object.values
返回对象 value
的数组,Object.entries
返回对象的键值对数组。它们都是是对象自身的(不含继承的)所有可遍历(enumerable)属性的键或值,但不包括 Symbol
属性。
与之配套的是 Object.getOwnPropertySymbols
,返回包含对象自身的所有 Symbol
属性键名的数组。还可以使用 Reflect.ownKeys
,返回包含对象自身的(不含继承的)所有键名的数组,不管键名是 Symbol
或字符串,也不管是否可枚举。
判断对象的属性是自身的属性和继承的属性,除了 hasOwnProperty
对象实例方法,ECMAScript 2022 引入 Object.hasOwn
,判断是否为自身的属性。Object.hasOwn
的一个好处是,对于不继承 Object.prototype
的对象不会报错,而 hasOwnProperty
是会报错的。
除了数组和对象,ECMAScript 2015+ 添加了新的数据结构 Map
、Set
。为了处理所有不同数据结构,就需要一种统一的接口机制。遍历器(Iterator)就是这样一种机制,它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
Iterator 的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口。
- 使得数据结构的成员能够按某种次序排列。
- 创造了一种新的遍历命令
for...of
循环,Iterator
接口供for...of
消费。
迭代器
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。具体来说,迭代器是通过具有 next()
方法来实现 Iterator
协议的任何对象,该方法返回具有两个属性的对象:
value
:迭代序列中的next
值。done
:如果已经迭代到序列中的最后一个值,则为true
。如果value
与done
一起出现,它则是迭代器的返回值。
创建迭代器对象后,可以通过重复调用 next()
来显式迭代。对迭代器进行迭代被称为消耗迭代器,因为它通常只能执行一次。产生终止值后,对 next()
的其他调用应继续返回 {done: true}
。
生成器函数
虽然自定义迭代器是一个有用的工具,但由于需要显式维护其内部状态,因此它们的创建需要仔细编程。生成器函数提供了一个强大的替代方案:它们允许您通过编写执行不连续的单个函数来定义迭代算法。生成器函数使用 function*
语法编写。
调用时,生成器函数最初并不执行其代码。相反,它们返回一种特殊类型的迭代器,称为生成器。当通过调用生成器的 next
方法使用某个值时,生成器函数将执行直到遇到 yield
关键字。该函数可以根据需要多次调用,并且每次都会返回一个新的生成器。每个生成器只能迭代一次。
迭代协议
迭代协议不是新的内置函数或语法,而是协议。这些协议可以由任何对象通过遵循一些约定来实现。迭代协议具体分为两个协议有两种协议:可迭代协议和迭代器协议。
可迭代协议
可迭代协议(iterable protocol)允许 JavaScript 对象定义或自定义其迭代行为,例如在 for...of
构造中循环哪些值。某些内置类型是具有默认迭代行为的内置可迭代对象,例如 Array
或 Map
,而其他类型则不是。
为了可迭代,对象必须实现 @@iterator
方法,这意味着该对象(或者它原型链上的某个对象)必须具有带有 @@iterator
键的属性,该属性可通过常量 Symbol.iterator
获得:
[Symbol.iterator]
:一个无参数的函数,其返回值为一个符合迭代器协议的对象。
每当需要迭代对象时,都会不带参数调用其 @@iterator
方法,并使用此方法返回的迭代器获得要迭代的值。值得注意的是调用此无参数函数时,它将作为对可迭代对象的方法进行调用。因此,在函数内部, this
关键字可用于访问可迭代对象的属性,以决定在迭代期间提供什么。
该函数可以是普通函数,也可以是生成器函数,以便在调用时返回迭代器对象。在此生成器函数内部,可以使用 yield
提供每个条目。
迭代器协议
迭代器协议(iterator protocol)定义了一种生成值序列(有限或无限)的标准方法,并且在生成所有值时可能会返回一个返回值。当对象实现具有以下语义的 next()
方法时,该对象就是迭代器:
next()
:无参数或者接受一个参数的函数,并返回符合IteratorResult
接口的对象。如果在使用迭代器内置的语言特征,得到一个非对象返回值,将会抛出TypeError("iterator.next() returned a non-object value")
。
所有迭代器协议方法(next()
、return()
和 throw()
)都应返回实现 IteratorResult
接口的对象。它必须具有以下属性:
done
:如果迭代器能够生成序列中的下一个值,则为false
的布尔值。如果迭代器已完成其序列,则具有值true
。在这种情况下,value
可以选择指定迭代器的返回值。value
:迭代器返回的任何 JavaScript 值。当done
为 true 时可以省略。
实际上,这两种属性都不是严格要求的。如果返回没有任一属性的对象,则它实际上相当于 { done: false, value: undefined }
。如果迭代器返回 done: true
的结果,则对 next()
的任何后续调用也预计会返回 done: true
,尽管这在语言级别上不强制执行。
next
方法可以接受一个值,该值将提供给方法体。任何内置的语言特征都将不会传递任何值。传递给生成器 next
方法的值将成为相应 yield
表达式的值。或者,迭代器还可以实现 return(value)
和 throw(exception)
方法,这些方法在被调用时告诉迭代器调用者已完成迭代,并且可以执行任何必要的清理。
return(value)
:无参数或者接受一个参数的函数并返回符合IteratorResult
接口的对象,通常value
等于传入的value
和done
。调用此方法告诉迭代器调用者不打算进行任何更多的next()
调用,并且可以执行任何清理操作。throw(exception)
:无参数或者接受一个参数的函数,并返回符合IteratorResult
接口的对象,通常done
等于true
。调用此方法告诉迭代器调用者检测到错误条件,并且exception
通常是Error
实例。
异步迭代器和异步可迭代协议
还有另一对用于异步迭代的协议,称为异步迭代器(async iterator)和异步可迭代协议(async iterable protocols)。与可迭代和迭代器协议相比,它们具有非常相似的接口,只是调用迭代器方法的每个返回值都包装在一个 Promise
中。当对象实现以下方法时,它就实现了异步可迭代协议:
[Symbol.asyncIterator]
:返回对象的无参数函数,并且符合异步迭代器协议。
当对象实现以下方法时,它就实现了异步迭代器协议:
next()
:无参数或者接受一个参数的函数,并返回promise
。Promise
会满足一个符合IteratorResult
接口的对象,并且属性与同步迭代器的属性具有相同的语义。return(value)
:无参数或者接受一个参数的函数,并返回promise
。Promise
会满足一个符合IteratorResult
接口的对象,并且属性与同步迭代器的属性具有相同的语义。throw(exception)
:无参数或者接受一个参数的函数,并返回promise``。Promise
会满足一个符合IteratorResult
接口的对象,并且属性与同步迭代器的属性具有相同的语义。
语言和迭代协议之间的交互
String
、Array
、TypedArray
、Map
、Set
以及 Intl.Segments
都是内置的可迭代对象,因为它们的每个 prototype
对象都实现了 @@iterator
方法。此外,arguments
对象和一些 DOM
集合类型,如 NodeList
也是可迭代的。ReadableStream
是唯一的内置异步可迭代对象。
生成器函数返回生成器对象,它们是可迭代的迭代器。异步生成器函数返回异步生成器对象,它们是异步可迭代迭代器。
从内置迭代器返回的迭代器实际上都继承自一个公共类 Iterator
,该类实现了 [Symbol.iterator]() { return this; }
方法,使它们都是可迭代迭代器。
有许多 API 接受可迭代对象,包括:Map()
、WeakMap()
、Set()
、WeakSet()
、Promise.all()
、Promise.allSettled()
、Promise.race()
、Promise.any()
、Array.from()
、Object.groupBy()
、Map.groupBy()
。