探秘遍历器 Iterator

500 阅读7分钟

Iterator.png

一、遍历器 Iterator

1. 什么是遍历器

  • 遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制
  • 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作,称可遍历的(iterable)
  • Iterator 接口主要供 ES6 for...of 使用。当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

2. 遍历器接口怎么实现的

ES6 规定默认 Iterator 接口部署在 Symbol.iterator 属性上,它是一个函数,返回一个遍历器对象

遍历器对象 特征: 具有 next 方法,每次调用 next 方法都会返回一个代表当前成员的信息对象,具有 value 和 done 两个属性。

关系:实现 Iterator 接口 -> 部署 Symbol.iterator 属性 -> 该方法返回遍历器对象 -> 可遍历的 -> for...of 遍历

ES6 还提供了 11 个 内置的 Symbol 值,指向语言内部使用的方法来实现。ES6 之前并没有暴露给开发者。Symbol.iterator 属性就是一个 js 内置的、类型为 Symbol 的特殊值,指向该对象的默认遍历器方法

  1. 自定义 Iterator 接口的例子
let obj = {
  [Symbol.iterator]: function () {
    let i = 0;
    return {
      next: function () {
        if (i < 3) {
          i++;
          return {
            value: i,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  },
};
for (let i of obj) {
  console.log(i); // 1 2 3 (注意done为true时,for循环终止,不会获取本次return结果!)
}
  1. 模拟遍历器的 next 操作过程,来遍历数组
function makeIterator(array) {
  let nextIndex = 0;
  return {
    next: function () {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { value: undefined, done: true };
    },
  };
}

let it = makeIterator(["a", "b"]);
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }

但是此时不可以 for..of 遍历,因为并不是部署在Symbol.iterator 属性上的。

3. 简单了解:迭代协议

迭代协议

  • 可迭代协议: 允许 JS 对象定义它们的迭代行为,即成为可迭代对象
    • 可迭代对象: 必须实现 @@iterator 方法, 可通过常量 Symbol.iterator 访问
  • 迭代器协议: 实现了一个拥有以下语义的 next() 方法
    • next()方法: 返回一个拥有 value、done 属性的对象

@@符号,JS 中通过 Symbol 构造函数公开,例如,@@toPrimitive 公开为 Symbol.toPrimitive

二、具备 Iterator 接口的数据结构

  • ES6 的有些数据结构原生具备 Iterator 接口(部署了 Symbol.iterator 属性)返回一个遍历器对象
  • 即不用任何处理,就可以被 for...of 循环遍历
  • 但有一些数据结构没有部署(比如对象)

原生具备 Iterator 接口的数据结构:

  • Array
  • String
  • Map
  • Set
  • TypedArray
  • 类似数组的对象:函数的 arguments 对象、NodeList 对象
  • 生成器 Generator

1. Array.prototype[Symbol.iterator]()

  1. 数组原生具有遍历器接口,部署在 Symbol.iterator 属性上。调用这个属性就得到遍历器对象
  2. 数组的遍历器接口只返回具有数字索引的属性
let arr = ["a", "b", "c"];
let it = arr[Symbol.iterator](); //  Array Iterator {}
it.next(); // { value: 'a', done: false }

Array.prototype[Symbol.iterator](); // 返回遍历器对象 Array Iterator {}

// 只返回具有数字索引的属性
let arr = [3, 5, 7];
arr.foo = "hello";
for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}
  1. ES6 新增的方法,也会返回 Array Iterator 对象

Array.prototype.keys() 对键名的遍历 Array.prototype.values() 对键值的遍历 Array.prototype.entries() 对键值对的遍历

let array1 = ["a", "b", "c"];
let iterator = array1.entries();

for (let [key, value] of iterator) {
  console.log([key, value]);
  // [0,'a']
  // [1,'b']
  // [2,'c']
}

// 也可以手动遍历
iterator.next();

2. String.prototype[Symbol.iterator]()

字符串也是一个类似数组的对象,也原生具有 Iterator 接口。 String 的默认迭代器会依次返回该字符串的各码点(code point)

let someString = "hi";
let i = someString[Symbol.iterator](); // String Iterator {}
i.next(); // {value:'h', done:false}
i.next(); // {value:'i', done:true}
i.next(); // {value:undefined, done:true}

例子:覆盖原生的 Symbol.iterator 方法、修改遍历器行为

let str = new String("hi");
[...str]; // ["h", "i"]

// 重新定义的 Symbol.iterator 方法,会影响原来内置语法结构的行为!
str[Symbol.iterator] = function () {
  return {
    // 只返回一次字符串 "bye"
    next: function () {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true,
  };
};

[...str]; // ["bye"]

3. 类似数组的对象

  1. DOM NodeList 对象 是类似数组的对象,具有遍历接口,可以直接遍历
  2. 函数的 arguments 对象 也是类似数组的对象,具有遍历接口,可以直接遍历
  3. 对于其他类似数组的对象,若想部署 Iterator 接口有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Symbol.iterator 方法。但普通对象 部署 数组的Symbol.iterator 方法,并无效果
// 1. 函数的 arguments 对象
(function () {
  for (let argument of arguments) {
    console.log(argument); // 1 2 3
  }
})(1, 2, 3);

// 2. nodeList 对象
let nodeList = document.querySelectorAll("div");
for (let item of nodeList) {
  console.log(item); // 打印页面的div元素
}

// 3. array like object
let arrLikeObj = {
  0: "aaa",
  1: "bbb",
  length: 2,
};
for (let item of arrLikeObj) {
  console.log("item:", item); // 报错 : arrLikeObj is not iterable
}

// 4. 部署 Iterator 接口: 1)设置 Symbol.iterator方法 2)Array.from方法将其转为数组
arrLikeObj[Symbol.iterator] = Array.prototype[Symbol.iterator];

for (let item of arrLikeObj) {
  console.log(item); // aaa bbb
}
// 或者
for (let item of Array.from(arrLikeObj)) {
  console.log(item); // aaa bbb
}

4. 对象(原生没有部署遍历器)

  1. 对于对象这种没有部署 Iterator 接口的数据结构,都需要自己在 Symbol.iterator 属性上面部署,这样才会被 for...of 循环遍历。
let obj = {
  data: ["hello", "world"],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false,
          };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};
for (let item of obj) {
  console.log(item); // "hello" "world"
}
  1. 对象为什么没有部署遍历器接口?

思考原因如下:

  • 使用 for...of 来进行对象的默认行为的遍历,要考虑的情况很多,比如 own property 还是原型上属性、是否可枚举、遍历 key/value 不同组合形式等,很难定义默认行为
  • ES6 原生提供了 Map 结构,完善了对象的用法,也可以使用 keys()/values()/entries() 完成各种形式的遍历。Map 的遍历顺序就是插入顺序。

所以如何实现对象的遍历,给我们的可用场景其实很多: ↓↓↓

for (const k of Object.keys(obj)) ... // enumerable own keys
for (const [k, v] of Object.entries(obj)) ... // enumerable own [key, value]
for (const k of Object.getOwnPropertyNames(obj)) // all own keys
for (const s of Object.getOwnPropertySymbols(obj)) // all own symbols
for (const k of Reflect.ownKeys(obj)) // all own keys (include symbo

Why are Objects not Iterable in JavaScript?

5. Map 和 Set 结构

  1. Set 和 Map 的实例(new Map()),原生具有 Iterator 接口
  2. 遍历的顺序是按照各个成员被添加进数据结构的顺序
  3. Map 和 Set 的 3 个实例方法,也都返回遍历器,都可以使用 for...of 遍历
    • Map.prototype.keys()/values()/entries()
    • Set.prototype.keys()/values()/entries()
  4. Set 结构遍历时,返回的是一个值;而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
// 1. set实例
let set = new Set([1, 2, 3, 3]); // Set(3){1,2,3}
for (let i of set) {
  console.log(i); // 1 2 3
}
// 实例方法返回的也是遍历器
set.values(); // SetIterator {1,2,3}
for (let i of set.values()) {
  console.log(i); // 1 2 3
}

// 2. map实例
let map = new Map([
  ["name", 1],
  ["age", 2],
]); // Map(2) {"name" => 1, "age" => 2}
for (let i of map) {
  console.log(i); // [name,1] [age,2]
}
// 实例方法返回的也是遍历器
map.keys(); // MapIterator  {name,age}
for (let i of map.keys()) {
  console.log(i); // name age
}
map.entries(); // MapIterator {"name" => 1, "age" => 2}
for (let i of map.entries()) {
  console.log(i); // [name,1] [age,2] √
}
  1. Set 的默认遍历器接口,就是它的 values 方法;Map 的默认遍历器接口,就是它的 entries 方法。这意味着,可以省略 values / entries 方法,直接用 for...of 循环遍历 Set/ Map。
Set.prototype[Symbol.iterator] === Set.prototype.values; // true
Map.prototype[Symbol.iterator] === Map.prototype.entries; // true

6. 使用生成器 Generator

1. 基础

  • Generator 函数是一个状态机,封装了多个内部状态
  • 执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
  • 特征
    • function 关键字与函数名之间有一个星号
    • 函数体内部使用 yield 表达式,定义不同的内部状态(yield : “产出”)
  • 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是返回一个指向内部状态的指针对象(遍历器对象)。必须调用遍历器对象的 next 方法,使得指针移向下一个状态
function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
}

let hw = helloWorldGenerator();
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }

2. 与 Iterator 接口的关系

由于 Generator 函数就是遍历器生成函数(返回一个遍历器对象),因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
// myIterable对象具有了 Iterator 接口,可以被...运算符遍历
[...myIterable]; // [1, 2, 3]

for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

3. 遍历器对象的 return()throw()

  • return() 方法必须返回一个对象,这是 Generator 语法决定的
    • 如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句),就会调用 return()方法
    • 如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return()方法
  • throw() 方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。可参阅《阮一峰-Generator 函数》一章。
let obj = {
  data: ["a", "b", "c", "d"],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false,
          };
        } else {
          return { value: undefined, done: true };
        }
      },
      return() {
        console.log("done");
        return { done: true };
      },
    };
  },
};

for (let i of obj) {
  console.log(i);
  break;
}

三、调用 Iterator 接口的场景

有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法)

1. 解构赋值

数组Set 结构 进行解构赋值时,会默认调用 Symbol.iterator 方法。

let set = new Set().add("a").add("b").add("c"); // Set(3) {"a", "b", "c"}
let [x, y] = set; // x='a'; y='b'
let [first, ...rest] = set; // first='a'; rest=['b','c'];

注意对象的解构赋值不行,因为 Object 没有部署 Iterator 接口

2. 扩展运算符

任何部署了 Iterator 接口的数据结构,都可以用扩展运算符转为真正的数组

// 字符串转数组
var str = "hello";
[...str]; //  ['h','e','l','l','o']

// 类数组对象转数组
let nodeList = document.querySelectorAll("div");
[...nodeList];

//  Generator 函数
const go = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...go()]; // [1, 2, 3]

3. yield* 表达式

yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

let generator = function* () {
  yield 1;
  yield* [2, 3, 4];
  yield 5;
};

var iterator = generator();

iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 4, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: undefined, done: true }

4. 接受数组作为参数的场合

任何接受数组作为参数的场合,其实都调用了遍历器接口(数组的遍历会调用遍历器接口)

  • Array.from()
  • new Map(), new Set(), new WeakMap(), new WeakSet()
  • Promise.all()、Promise.race()
  • for...of

1. Array.from()

部署了 Iterator 接口的数据结构,Array.from 都能将其转为数组

Array.from("hello");
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(["a", "b"]);
Array.from(namesSet); // ['a', 'b']

2. for...of

for...of 循环内部调用的是数据结构的 Symbol.iterator 方法

  1. 特点
  • 可以使用 break, continuereturn 终止循环
  • 生成器不应该重用。在退出循环后,生成器关闭,并尝试再次迭代,不会产生任何进一步的结果。
let array1 = ["a", "b", "c"];
let iterator = array1.entries();

for (let [key, value] of iterator) {
  console.log([key, value]);
}

// 假如继续调用
iterator.next(); // {value: undefined, done: true}
  1. for...offor...in 的区别
  • for...in
    • 循环主要是为遍历对象而设计的,不适用于遍历数组
    • for...in 语句以任意顺序 遍历对象的可枚举属性
    • for...in 循环读取键名
  • for...of 循环读取键值
    • for...of 语句遍历可迭代对象
    • for...of 循环读取键值
    • 如果要通过 for...of 循环获取数组的索引,可以借助数组实例的 entries 方法keys 方法

底层原理可参考:

Array Iterator Objects