title: 前端开发系列125-进阶篇之Iterator
tags:
categories: []
date: 2019-08-01 00:00:08
本文简单说明[ 迭代器接口 Iterator]() 接口的基本使用,涉及 Array 、Set 、Map 和 String 以及伪数组等数据结构,以及 `for...of`循环的用法等。
Iterator - 基本使用

Iterator 是一种接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署实现了 Iterator 接口,就可以完成遍历操作。

Iterator 的优点在于能够为不同的数据结构提供了统一的接口;能够以特定的排序来遍历数据结构;提供创造了for...of循环。JavaScript中默认实现迭代器接口( Iterator )的数据结构有类数组结构(NodeList、arguments、String等) 和 Set 、Map、Array等 ,实现 Iterator 接口的数据结构均支持使用 for...of 循环来执行遍历操作。

下面通过代码简单展示Set 、Map、Array三种数据结构中实现的原生迭代器接口( Iterator )和for...of遍历

/* 1.数组 Array */
/* 2.集合 Set */
/* 3.映射 Map */
/* 4.其它结构 */

let arr = [100, 200, 300];
console.log("arr", arr);

let set = new Set([10, 20, 30, 20, "测试"]);
console.log("set", set);

let map = new Map();
map.set("a", "A");
map.set("b", "B");
map.set("c", "C");
console.log("map", map);

/* 测试数据 */
for (let ele of arr) {
    console.log(ele);
}
console.log('______________');
for (let ele of set) {
    console.log(ele);
}
console.log('______________');
for (let [key,val] of map) {
    console.log(key,val);
}
console.log('______________');

/* 打印输出 */
/* 
arr [ 100, 200, 300 ]
set Set { 10, 20, 30, '测试' }
map Map { 'a' => 'A', 'b' => 'B', 'c' => 'C' }
100
200
300
______________
10
20
30
测试
______________
a A
b B
c C
______________ */

通过查看console.log(Array.property,Set.property,Map.property);打印结果,你会发现在数组、集合和映射它们的内部,都在其构造函数的原型对象上无一例外都实现了Symbol(Symbol.iterator): ƒ entries()函数,调用该函数我们能够得到一个iterator 型对象,当我们使用for...of循环结构来遍历它们的时候,在内部会利用该对象来完成遍历操作。

let arr = [100, 200, 300];

/* arr.__proto__ ===  Array.prototype[Symbol.iterator] */
let iterator = arr[Symbol.iterator]();
console.log(iterator); /* Object [Array Iterator] {} */

console.log("_________bgn_________")
let o = iterator.next();
while (!o.done) {
    o = iterator.next()
    console.log(o);
}
console.log("_________end_________")

/* 打印输出 */
/* 
Object [Array Iterator] {}
_________bgn_________
{ value: 200, done: false }
{ value: 300, done: false }
{ value: undefined, done: true }
_________end_________
*/

通过Array.prototype[Symbol.iterator]()可以得到一个iterator 型对象,调用该对象的next方法后能得到个拥有两个键值对的对象,其中value表示的是当前的值,而 done 可以理解为是循环是否结束。在上面的代码中,我通过一个 while 循环来模拟了for..of循环过程。此外,也可以简单对比下这些结构中的entries()、keys() 和 values()等函数的用法。

let arr = [100, 200, 300];
let iterator = arr.entries();

console.log("_________bgn_________")
let o = iterator.next();
while (!o.done) {
    o = iterator.next()
    console.log(o);
}
console.log("_________end_________")
console.log(arr.keys())
console.log(arr.values())

/* 测试输出 */
/* 
  _________bgn_________
  { value: [ 1, 200 ], done: false }
  { value: [ 2, 300 ], done: false }
  { value: undefined, done: true }
  _________end_________
  Object [Array Iterator] {}
  Object [Array Iterator] {} */

在 ES6中的数组、Set和 Map 中都部署了entries() 、keys()、values() 三个方法,它们调用后都返回 iterator 迭代器对象,其中entries()返回的迭代器对象用于遍历[key,value]组成的数组,而keys()返回的迭代器对象用于遍历所有的键名,values()返回的迭代器对象用于遍历所有的键值。除了上面列出的Array、Set和 Map结构支持for..of外,下面在给出类数组结构(伪数组)的几种情况。

/* 1. arguments */
function test() {
    console.log('arguments');
    for (const iterator of arguments) {
        console.log('iterator = ', iterator);
    }
}

test("a", "b", "c", 10, 203);

/* 打印输出: */
/* 
arguments
iterator =  a
iterator =  b
iterator =  c
iterator =  10
iterator =  203 */


/* 2.NodeList */
let oDiv = document.createElement("div");
oDiv.innerHTML = "<span>A</span><span>B</span><span>c</span><span>D</span>";
console.log(oDiv.children);
for (const iterator of oDiv.children) {
    console.log('element = ', iterator);
}
/* 打印输出 */
/* 
HTMLCollection(4) [span, span, span, span]
VM76:5 element =  <span>​A​</span>
VM76:5 element =  <span>​B​</span>
VM76:5 element =  <span>​c​</span>
VM76:5 element =  <span>​D​</span> */


/* 3.字符串(String) */
let str = "Hello";

for (const iterator of str) {
    console.log("s = ", iterator);
}

for (const iterator of str[Symbol.iterator]()) {
    console.log("s = ", iterator);
}

/* 打印输出 */
/* 
s =  H
s =  e
s =  l
s =  l
s =  o */

在上面列出的几种伪数组结构中,他们内部都实现了iterator接口,自己写的伪数组或者是对象实现了iterator接口支持for...of循环吗? 答案是否定的。

/* 1、自己写的伪数组结构 */
let likeArray = { 0: "a", 1: "b", 2: 'c', length: 3 };
for (const iterator of likeArray) {
    console.log('iterator = ', iterator);
}
/* 报错:TypeError: likeArray is not iterable */

/* 2.对象结构 */
let o = { name: "Yong", age: 18 };
for (const iterator of o) {
    console.log('iterator = ', iterator);
}
/* 报错:TypeError: o is not iterable */

如果自己写的伪数组也要能够支持for...of 循环,那么可以有下面几种尝试的办法。

let likeArray = { 0: "a", 1: "b", 2: 'c', length: 3 };

/* 第一种方式:通过对象解构方式来先转换为数组 */
/* 结果:失败 (分析原因:扩展运算符[...]内部默认会自动调用 iterator 接口) */
for (const iterator of[...likeArray]) {
    console.log('iterator = ', likeArray);
}

/* 第二种方式:利用 Array.from尝试转换为数组 */
for (const iterator of Array.from(likeArray)) {
  console.log('iterator = ', iterator);
}

/* 结果输出:
iterator =  a
iterator =  b
iterator =  c */

/* 第三种方式:在当前伪数组的原型上面部署"原生"的 iterator 迭代器接口 */
/* ①  */
// likeArray.__proto__[Symbol.iterator] = Array.prototype[Symbol.iterator];
/* ② */
// Object.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
/* ③ */
// likeArray[Symbol.iterator] = Array.prototype[Symbol.iterator];
/* ④ */
likeArray[Symbol.iterator] = [][Symbol.iterator].bind(likeArray);

for (const iterator of likeArray) {
    console.log('iterator = ', iterator);
}

/* 结果输出:
iterator =  a
iterator =  b
iterator =  c */

注意的是上面直接利用数组的[Symbol.iterator]来为伪数组部署迭代器接口的方式并不适用于普通的对象,如果用在普通对象的身上那么则毫无效果。
/* 2.对象结构 */
let o = { name: "Yong", age: 18 };

/* 2.1 对象无法直接通过 for...of进行遍历 */
for (const iterator of o) {
    console.log('iterator = ', likeArray);
}
/* 报错:TypeError: o is not iterable */

/* 2.2 尝试利用数组的Symbol.iterator接口部署 */
o.__proto__[Symbol.iterator] = [][Symbol.iterator];
for (const iterator of o) {
    console.log('iterator = ', likeArray);
}
/* 结果:不会进入循环,没有任何输出 */

/* 2.3 尝试遍历对象的 keys 间接遍历对象 */
for (const key of Object.keys(o)) {
    console.log(`key:${key} value:${o[key]}`);
}
/* 结果 */
/* 
key:name value:Yong
key:age  value:18 */

这里简单思考和总结下,对象中没有实现Iterator迭代器的原因
○ 对象已经拥有了 for...in循环 (该循环专为对象迭代设计)。
○ 对象在遍历的时候,属性( 键值对 )遍历的先后顺序是不确定的,而Iterator迭代器是线性的。
○ ES6提供了 Map ,可以在某种程度上实现替代操作。

Iterator - 内部结构

在数组等数据结构中,当我们调用 entries() 或者是Symbol.iterator()的时候将得到一个iterator迭代器对象,在该对象中next方法每调用一次就会返回一个包含本次迭代 value 值以及标记是否完成迭代的 done 属性。

let arr = ["a", "b"];
let iterator = arr[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
/* 打印输出 */
/* 
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true } */

这里我们可以尝试来封装一个函数makeIterator,模拟 next函数的工作过程。

let makeIterator = (arr) => {
    let idx = 0;
    return {
        next: () => idx < arr.length ? 
        { value: arr[idx], done: false } : { value: undefined, done: true }
    }
}

let iterator = makeIterator([100, 200, "Yong"]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

/* 打印输出 */
/* 
{ value: 100, done: false }
{ value: 200, done: false }
{ value: 'Yong', done: false }
{ value: undefined, done: true }*/

假如我们想要让普通的对象也能够直接支持(除了Object.keys()形式)for...of循环,那么可以考虑主动的在对象或者对象的原型对象上面部署iterator迭代器接口,下面简单给出对应的示例代码。

/* 方案-01 */
// let o = {
//     name: "Yong",
//     address: "GuangZhou",
//     [Symbol.iterator]() {
//         let idx = 0;
//         let map = [];
//         Object.keys(this).forEach(key => map.push([key, this[key]]))
//         return {
//             next: () => idx < map.length ? { value: map[idx++], done: false } 
//              : { value: undefined, done: true }
//         };
//     }
// };

/* 方案-02 */
Object.prototype[Symbol.iterator] = function() {
    let idx = 0;
    let map = [];
    Object.keys(this).forEach(key => map.push([key, this[key]]))
    return {
        next: () => idx < map.length ? { value: map[idx++], done: false } 
          : { value: undefined, done: true }
    };
}

let o = {
    name: "Yong",
    address: "GuangZhou"
};

/* 测试代码 */
let iterator = o[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log("+++++++++++++++++");

for (const iterator of o) {
    console.log('iterator:', iterator);
}

console.log("+++++++++++++++++");

for (const [key, value] of o) {
    console.log('key:', key, "val:", value);
}

/* 打印输出 */
/* 
{ value: [ 'name', 'Yong' ], done: false }
{ value: [ 'address', 'GuangZhou' ], done: false }
{ value: undefined, done: true }

+++++++++++++++++
iterator: [ 'name', 'Yong' ]
iterator: [ 'address', 'GuangZhou' ]

+++++++++++++++++
key: name    val: Yong
key: address val: GuangZhou */

如果想要更简单点,其实还可以借助 Generator 生成器函数来实现。

/* 生成器函数来实现 */
let obj = {
    *[Symbol.iterator]() {
        yield "H";
        yield "e";
        yield "l";
        yield "l";
        yield "o"
    }
}

for (const e of obj) {
    console.log('e:', e);
}
/* 打印输出 */
/* 
e: H
e: e
e: l
e: l
e: o */