ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## Iterator和for...of ### Iterator(遍历器)的概念 JavaScript表示集合的数据结构有数组和对象,ES6又增加了 `Set` 和 `Map` ,4种数据结构,并且它们之间还可以相互嵌套。 `Iterator` 就是一种统一的机制,用来访问这些不同的数据结构。 `Iterator` 的遍历过程如下: - 创建一个对象指针,指向当前数据结构的起始位置,其实它就是一个指针对象 - 第一次调用指针对象的 `next` 方法,指针指向数据结构的第一个成员 - 第二次调用时,指针指向第二个成员 - 以此类推,直到指向最后一个成员 每次调用 `next` 方法都会返回数据结构当前成员的信息,这个信息是一个对象,包含了 `value` 和 `done` 两个属性, `value` 表示当前成员的值, `done` 表示遍历是否结束,是一个布尔值。 下面来模拟一下这个过程: ```js let it = makeIterator(['a', 'b']) it.next() // { value: 'a', done: false } it.next() // { value: 'b', done: false } it.next() // { value: undefined, done: true } function makeIterator (array) { var nextIndex = 0 return { next: function () { return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true } } } } ``` 指针对象的 `next` 方法用于移动指针,开始时,指针指向数组的开始位置,然后,每次调用 `next` 方法,指针都会指向数组的下一个成员。 ### 默认Iterator接口 `Iterator` 接口的目的是为所有的数据结构提供一种统一的访问机制,即 `for...of`循环。 ES6规定,默认的 `Iterator` 接口部署在数据结构的 `Symbol.iterator` 属性上,一个数据结构只要具有 `Symbol.Iterator` 属性,就可以认为是可遍历的。 调用 `Symbol.iterator` 方法,我们就可以得到当前数据结构默认的遍历器生成函数。它本身是一个表达式,返回 `Symbol` 对象的 `iterator` 属性。 ```js const obj = { [Symbol.iterator]: function () { return { return { value: 1, done: true } } } } ``` ES6的有些数据结构原生具有 `Iterator` 结构,比如数组,即数据可以不做任何处理就可以被 `for...of` 循环遍历。但对象没有。原生具有遍历器属性的数据结构如下: - Array - Map - Set - String - TypedArray - arguments - NodeList ```js let arr = ['a', 'b', 'c'] let iter = arr[Symbol.iterator]() iter.next() // [value: 'a', done: false] iter.next() // [value: 'b', done: false] iter.next() // [value: 'c', done: false] iter.next() // [value: undefined, done: true] ``` 对于原生部署 `Iterator` 接口的数据结构,我们不用自己编写遍历器生成函数, `for...of`循环会自动遍历它们。对象(Object)之所有没有默认部署 `Iterator` 接口,是因为对象属性的遍历先后顺序是不确定的,需要开发者手动指定。 一个对象如果要具备可被 `for...of` 循环调用的 `Iterator` 接口,就必须在 `Symbol.iterator` 属性上部署遍历器生成方法。 ```js class RangeIterator { constructor (start, stop) { this.value = start this.stop = stop } [Symbol.iterator]() { return this } next () { let value = this.value if (value < this.stop) { this.value++ return { done: false, value: value } } return { done: true, value: undefined } } } function range (start, stop) { return new RangeIterator(start, stop) } for (let value of range(0, 3)) { console.log(value) // 0, 1, 2 } ``` ### 调用Iterator接口的场合 - 解构赋值:对数组和 `Set` 结构进行解构赋值时,会默认调用 `Symbol.iterator` 方法 - 扩展运算符:扩展运算符(...)也会调用默认的 `Iterator` 接口 - `yield*` :`yield*` 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口 ### 字符串的 Iterator 接口 字符串是一个类似数组的对象,也具有原生的 `Iterator` 接口 ```js let someString = 'hi' typeof someString[Symbol.iterator] // "function" ``` ### 遍历器对象的 return()、throw() 遍历器对象除了具有 `next` 方法,还可以具有 `return` 方法和 `throw` 方法。 ### for...of 循环 ES6借鉴了其他语言的特性,引入了 `for...of` 循环作为遍历所有数据结构的统一的方法。 一个数据结构只要部署了 `Symbol.iterator` 属性,那么它就会被视为具有 `iterator` 接口,就可以使用 `for...of` 循环遍历它的成员。 `for...of` 循环可以使用的范围包括数组、`Set` 和 `Map` 结构、某些类似数组的对象,比如 `arguments` 对象、`DOM NodeList` 对象、`Generator` 对象,以及字符串。 #### 数组 数组原生具备 `iterator` 接口。 ```js const arr = ['red', 'green', 'blue'] for (let v of arr) { console.log(v) // red green blue } ``` #### Set和Map `Set` 和 `Map` 结构原生具有 `Iterator` 接口,可以直接使用 `for...of` 循环。 ```js let engines = new Set(['Gecko', 'Trident', 'Webkit', 'Webkit']) for (let e of engines) { console.log(e) } // Gecko // Trident // Webkit let es6 = new Map() es6.set('edition', 6) es6.set('committee', 'TC39') es6.set('standard', 'ECMA-262') for (let [name, value] of es6) { console.log(name + ":" + value) } ``` #### 类似数组的对象 ```js // 字符串 let str = 'hello' for (let s of str) { console.log(s) // h e l l o } // DOM NodeList对象 let paras = document.querySelectorAll('p') for (let p of paras) { p.classList.add('test') } // arguments对象 function printArgs () { for (let x of arguments) { console.log(x) } } printArgs('a', 'b') // 'a', 'b' ``` #### 对象 对于普通的对象,`for...of` 结构不能直接使用,否则会报错,必须部署了 `Iterator` 接口才能使用,在这种情况下,`for...in`仍然是可以使用的。 ```js let es6 = { edition: 6, committee: 'TC39', standard: 'ECMA-262' } for (let e in es6) { console.log(e) } // edition // committee // standard for (let e of es6) { console.log(e) } // TypeError: es6[Symbol.iterator] is not a function ``` 对于这种情况一般的解决办法是,使用 `Object.keys` 方法将对象的键名生成一个数组,然后遍历这个数组 ```js for (let key of Object.keys(someObject)) { console.log(key + ': ' + someObject[key]) } ``` 另外一个方法是使用 `Generator` 函数将对象重新包装一下 ```js function* entries(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]] } } for (let [key, value] of entries(obj)) { console.log(key, '->', value) } // a -> 1 // b -> 2 // c -> 3 ```