迭代器

我们经常使用 for 循环来遍历数组

let arr = ["red", "green", "blue"]
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

上述代码是使用变量 i 作为下标来访问数组,但是对于一些无序的集合,例如 Map 我们便不能通过下标的形式进行访问,所以有必要提供统一的方式来访问集合,这就是迭代器。

所谓的迭代器那当然是用来迭代用的,迭代的对象可以是数组,集合以及对象。迭代器含有一个方法 next(),该方法的作用就是从数组 (集合、对象) 取出一个值,它返回一个对象,对象中有两个属性:

  • done:为布尔值,false 表示集合中还有元素可遍历,true 表示集合中没有元素可遍历
  • value: 如果 done 为 false,value 的值为集合中的一个元素值,如果 done 为 true,value 的值为 undefined
function getIterator() {
let i = 0;
return {
next() {
return {
done: i > 3,
value: i > 3 ? undefined : i++
}
}
}
}

let iterator = getIterator();

console.log(iterator.next()); // { done: false, value: 0 }
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }

getIterator 返回一个具有 next 方法的对象,next 方法返回一个具有 done 和 value 属性的对象,每次调用 next 方法,都会对 i 进行 ++ ,当 i > 3 时,done 的值就为 true,且返回的 value 值为 undefined。

生成器

生成器的作用是来生成一个迭代器的,上面我们自定义的迭代器需要自己维护 done 和 value 属性,使用起来还是比较麻烦的。生成器的语法就是在 function 关键字后加一个 * 号,通过 yield 返回被迭代的元素

function *getIterator() {
let i = 0
while (i <=3) {
yield i++
}
}

let iterator = getIterator()

console.log(iterator.next()); // { done: false, value: 0 }
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }

当我们调用 getIterator 方法时,并不会执行该方法,而是会返回一个迭代器。每当该迭代器调用 next 方法时,就会开始执行函数,直至遇到 yield 语句,它会把 yield 后的表达式作为 value 属性的值返回,例如遇到yield 1 时,返回对象 {done: false, value: 1}

当返回对象后,不会继续执行函数,而是会挂起该函数,直到迭代器再次调用 next 方法时,就会从上次挂起的地方开始执行,直到遇到 yield 语句或者执行完函数。

当执行完函数时会返回 {done: true, value: undefined} ,如果是通过 return 语句结束函数的,例如 return 10 ,那么这时会返回 {done: true, value: 10}

因为生成器只是一个函数,所以可以有函数表达式的形式,它的函数表达式形式如下

let getIterator = function *() {
// do something
}

同样生成器也可以是对象的方法

let o = {
*getIterator() {

}
}
let iterator = o.getIterator()

当生成器作为对象的方法时,* 要放在方法名前,如 *getIterator

可迭代与 for…of 循环

可迭代的概念是针对对象提出,如果对象含有 Symbol.iterator 属性,那么就说它是可迭代的。Symbol.iterator 是一个函数,它返回一个迭代器,因此我们可以通过下面的方式去迭代一个对象

let obj = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return {
done: i > 3,
value: i > 3 ? undefined : i++
}
}
}
}
}

let iterator = obj[Symbol.iterator]()
let result = iterator.next();
while(!result.done) {
console.log(result.value); // 1
// 2
// 3
result = iterator.next();
}

上面遍历对象还是比较麻烦,对于任意一个对象都需要写一些模板代码,如

let iterator = obj[Symbol.iterator]()
let result = iterator.next();
while(!result.done) {
// 拿到 result.value,然后做一些事情
result = iterator.next();
}

所以为了简化代码的编写,ES6 提出一个新的 for … of 语法糖来帮我们遍历可迭代的对象,上面遍历对象的代码可以写为如下

for(value of obj) {
console.log(value); // 1
// 2
// 3
}

代码简洁了很多,但是我们要明白 for … of 不是黑魔法,for … of 所做的事情同上面写的模板代码做的事情是一样的。

所有由生成器生成的迭代器都是可迭代的,因为生成器会默认为迭代器的 Symbol.iterator 属性赋值。

let getIterator = function *() {
yield 1
yield 2
}

let iterator = getIterator()
for (let value of iterator) {
console.log(value); // 1
// 2
}

我们可以通过判断对象的 Symbol.iteraor 是否为 function 来判断该对象是否可迭代

function isIterable(obj) {
return typeof obj[Symbol.iterator] === 'function';
}

let array = [1, 2, 3]
let map = new Map()
let set = new Set()
let weakMap = new WeakMap()
let weakSet = new WeakSet()

console.log(isIterable(array)); // true
console.log(isIterable(map)); // true
console.log(isIterable(set)); // true
console.log(isIterable(weakMap)); // false
console.log(isIterable(weakSet)); // false

内置的迭代器

集合迭代器

通过上面的代码我们知道数组、Map 以及 Set 这三个几个都是可迭代的,下面我们就将学习它们内置的迭代器。每一个集合都有如下三个方法:

  • entries
  • keys
  • values

这三个方法都会返回一个迭代器,entries 迭代器中的元素为键值对组成数组,keys 中的元素为集合中的键,values 中的元素为集合的值。

其实对于数组和 Set 来说没有键的概念,但是为了接口的统一,我们在这里定义一下数组与 Set 的键:数组的键就是下标 (0, 1, …),Set 的键与它的值相同。

定义如下集合

let arr = ["red", "green", "blue"]

let set = new Set(["Alice", "Bob"]);

let map = new Map();
map.set("Chinese", 88);
map.set("Math", 98);
map.set("English",59);

entries

for(let entry of arr.entries()) {
console.log(entry); // [ 0, 'red' ]
// [ 1, 'green' ]
// [ 2, 'blue' ]
}

for(let entry of set.entries()) {
console.log(entry); // [ 'Alice', 'Alice' ]
// [ 'Bob', 'Bob' ]
}

for(let entry of map.entries()) {
console.log(entry); // [ 'Chinese', 88 ]
// [ 'Math', 98 ]
// [ 'English', 59 ]
}

keys

for(let key of arr.keys()) {
console.log(key); // 0
// 1
// 2
}

for(let key of set.keys()) {
console.log(key); // Alice
// Bob
}

for(let key of map.keys()) {
console.log(key); // Chinese
// Math
// English
}

values

for(let value of arr.values()) {
console.log(value); // red
// green
// blue
}

for(let value of set.values()) {
console.log(value); // Alice
// Bob
}

for(let value of map.values()) {
console.log(value); // 88
// 98
// 59

现在还有一个问题就是集合它们的 Symbol.iterator 是什么,对于数组和 Set 来说,它们的 Symbol.iterator 与 values 相同,对于 Map 来讲,它们的 Symbol.iterator 与 entries 相同

for(let item of arr){
console.log(item); // red
// green
// blue
}

for(let item of set){
console.log(item); // Alice
// Bob
}

for(let item of map){
console.log(item); // [ 'Chinese', 88 ]
// [ 'Math', 98 ]
// [ 'English', 59 ]
}

字符串迭代器

字符串越来越像数组了,例如我们可以通过下标去访问字符串中的字符,所以我们可以通过下面的方式遍历字符串

const str = "Hi 𠮷"

for(let i = 0; i < str.length; i++) {
console.log(str[i]);
}

但是因为 JavaScript 不识别 4 个字节表示的字符,所以打印如下

H
i



所幸 ES6 对 Unicode 提供全套支持,所以 ES6 提供的字符串迭代器解决了这个问题

for(let c of str) {
console.log(c);
}
H
i

𠮷

展开运算符

看下面一个例子

function removeDuplicate(array) {
let set = new Set(array)
return [...set]
}

上面这个函数是对去除数组中的重复元素,在其中我们使用了 […set] 展开运算符,将 Set 集合展开为一个数组返回,那么 JavaScript 内部是怎么做的,其实内部默认调用了 Set 集合迭代器,然后将 Set 集合中的元素添加到数组中的

let arr = [...set]

// 等价于
let arr = []
for(let value of set) {
arr.push(value)
}

迭代器高级用法

为迭代器传入参数

我们在调用迭代器的 next 方法时,还可以向其中传入一个参数

function *getIterator() {
let first = yield 1;
yield first + 2;
}

let iterator = getIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(2)); // { value: 4, done: false }

上面的动图说明了代码的执行过程:当我们调用第一个 next 时,方法开始执行,当执行到 yield 1 后停止执行,挂起函数并将值返回,当我们第二次调用 next 时,这时我们传递一个参数,这个参数会被传递给 first,接着继续执行,当执行到 yield first + 2 挂起函数,并返回值。

抛出错误

迭代器还有一个 throw 方法,它可以抛出一个错误,具体的应用场景我还不清楚

function *getIterator() {
let first = yield 1;
try {
let second = yield first + 2
} catch (e) {
second = 6;
}
yield second
}

const iterator = getIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(2)); // { value: 4, done: false }
console.log(iterator.throw(new Error("error..."))); // { value: 6, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

委托生成器

有的时候希望能够组合两个迭代器,我们可以通过 yield* 的语句来组合两个迭代器

function *generator01() {
yield 1;
yield 2;
}

function *generator02() {
yield 3;
yield 4;
}

function *generatorCombine() {
yield *generator01();
yield *generator02();
}

for (value of generatorCombine()) {
console.log(value); // 1
// 2
// 3
// 4
}

yield *gengerator01() 表示委托 generator01 这个生成器来做事情,当执行到这条语句时,接下来会进入 generator01 内部去执行,直到遇到 yield 语句,则会将函数挂起,然后将值返回,当下次调用 next 方法时则从上次挂起的地方重新开始执行。