可迭代协议

规定了某个对象是否可以迭代
一个对象本身或者原型链上有 Symbol.iterator 方法
当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。

迭代协议

规定了某对象是否是迭代器
只有实现了一个拥有以下语义(semantic)的 **next()** 方法,一个对象才能成为迭代器:

迭代器

迭代器概念

所谓迭代器,其实就是一个具有 next() 方法的对象,每次调用 next() 都会按顺序返回数据结构的成员,该结果对象有两个属性,value 表示当前成员的值,done 表示遍历是否结束。
下面是一个模拟next方法返回值的例子。

  1. var it = makeIterator(['a', 'b']);
  2. it.next() // { value: "a", done: false }
  3. it.next() // { value: "b", done: false }
  4. it.next() // { value: undefined, done: true }
  5. function makeIterator(array) {
  6. var nextIndex = 0;
  7. return {
  8. next: function() {
  9. return nextIndex < array.length ?
  10. {value: array[nextIndex++], done: false} :
  11. {value: undefined, done: true};
  12. }
  13. };
  14. }

调用指针对象的next方法,就可以遍历事先给定的数据结构。

没有对应数据解构的遍历起对象

  1. var it = idMaker();
  2. it.next().value // 0
  3. it.next().value // 1
  4. it.next().value // 2
  5. // ...
  6. function idMaker() {
  7. var index = 0;
  8. return {
  9. next: function() {
  10. return {value: index++, done: false};
  11. }
  12. };
  13. }

默认迭代器接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。
严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
我们直接 for of 遍历一个对象,会报错,然而如果我们给该对象添加 Symbol.iterator 属性(原型链上的对象具有该方法也可):

  1. var obj = {
  2. a: 1,
  3. b: 2,
  4. c: 3,
  5. [Symbol.iterator] : function () {
  6. let i = 0;
  7. return {
  8. next: () => {
  9. // debugger
  10. return i < Object.values(this).length ?
  11. {
  12. value: Object.values(this)[i++]
  13. } : {
  14. done: true
  15. };
  16. }
  17. };
  18. }
  19. };
  20. var it = obj[Symbol.iterator]()
  21. it.next()
  22. it.next()
  23. for (let value of obj) {
  24. console.log(value)
  25. }
  26. // 1,2,3
  1. class RangeIterator {
  2. constructor(start, stop) {
  3. this.value = start;
  4. this.stop = stop;
  5. }
  6. [Symbol.iterator]() { return this; }
  7. next() {
  8. var value = this.value;
  9. if (value < this.stop) {
  10. this.value++;
  11. return {done: false, value: value};
  12. }
  13. return {done: true, value: undefined};
  14. }
  15. }
  16. function range(start, stop) {
  17. return new RangeIterator(start, stop);
  18. }
  19. for (var value of range(0, 3)) {
  20. console.log(value); // 0, 1, 2
  21. }

上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。

  1. let iterable = {
  2. 0: 'a',
  3. 1: 'b',
  4. 2: 'c',
  5. length: 3,
  6. [Symbol.iterator]: Array.prototype[Symbol.iterator]
  7. };
  8. for (let item of iterable) {
  9. console.log(item); // 'a', 'b', 'c'
  10. }

对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。
注意,普通对象部署数组的Symbol.iterator方法,并无效果。

  1. let iterable = {
  2. a: 'a',
  3. b: 'b',
  4. c: 'c',
  5. length: 3,
  6. [Symbol.iterator]: Array.prototype[Symbol.iterator]
  7. };
  8. for (let item of iterable) {
  9. console.log(item); // undefined, undefined, undefined
  10. }

使用 while 循环遍历

  1. var $iterator = ITERABLE[Symbol.iterator]();
  2. var $result = $iterator.next();
  3. while (!$result.done) {
  4. var x = $result.value;
  5. // ...
  6. $result = $iterator.next();
  7. }

调用 Iterator 接口的场合

解构赋值

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

  1. let set = new Set().add('a').add('b').add('c');
  2. let [x,y] = set;
  3. // x='a'; y='b'
  4. let [first, ...rest] = set;
  5. // first='a'; rest=['b','c'];

扩展运算符

扩展运算符(…)也会调用默认的 Iterator 接口。

  1. // 例一
  2. var str = 'hello';
  3. [...str] // ['h','e','l','l','o']
  4. // 例二
  5. let arr = ['b', 'c'];
  6. ['a', ...arr, 'd']
  7. // ['a', 'b', 'c', 'd']

yield*

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

  1. let generator = function* () {
  2. yield 1;
  3. yield* [2,3,4];
  4. yield 5;
  5. };
  6. var iterator = generator();
  7. iterator.next() // { value: 1, done: false }
  8. iterator.next() // { value: 2, done: false }
  9. iterator.next() // { value: 3, done: false }
  10. iterator.next() // { value: 4, done: false }
  11. iterator.next() // { value: 5, done: false }
  12. iterator.next() // { value: undefined, done: true }

其他场合

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

    字符串的 Iterator 接口

    是一个类似数组的对象,也原生具有 Iterator 接口。
    1. var someString = "hi";
    2. typeof someString[Symbol.iterator]
    3. // "function"
    4. var iterator = someString[Symbol.iterator]();
    5. iterator.next() // { value: "h", done: false }
    6. iterator.next() // { value: "i", done: false }
    7. iterator.next() // { value: undefined, done: true }
    上面代码中,调用Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。
    可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。
    1. var str = new String("hi");
    2. [...str] // ["h", "i"]
    3. str[Symbol.iterator] = function() {
    4. return {
    5. next: function() {
    6. if (this._first) {
    7. this._first = false;
    8. return { value: "bye", done: false };
    9. } else {
    10. return { done: true };
    11. }
    12. },
    13. _first: true
    14. };
    15. };
    16. [...str] // ["bye"]
    17. str // "hi"
    上面代码中,字符串 str 的Symbol.iterator方法被修改了,所以扩展运算符(...)返回的值变成了bye,而字符串本身还是hi

    iIterator接口与 Generator 函数

Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。

  1. var myIterable1 = {
  2. [Symbol.iterator]: function () {
  3. var nextIndex = 0;
  4. var array = [1,2,3]
  5. return {
  6. next: function () {
  7. return nextIndex < array.length ?
  8. {
  9. value: [1,2,3][nextIndex++],
  10. done: false
  11. } : {
  12. value: undefined,
  13. done: true
  14. }
  15. }
  16. }
  17. }
  18. }
  19. //
  20. var it = myIterable1[Symbol.iterator]();
  21. it.next()
  22. it.next()
  23. it.next()
  24. [...myIterable1]
  25. let myIterable = {
  26. [Symbol.iterator]: function* () {
  27. yield 1;
  28. yield 2;
  29. yield 3;
  30. }
  31. }
  32. [...myIterable] // [1, 2, 3]
  33. // 或者采用下面的简洁写法
  34. let obj = {
  35. * [Symbol.iterator]() {
  36. yield 'hello';
  37. yield 'world';
  38. }
  39. };
  40. for (let x of obj) {
  41. console.log(x);
  42. }
  43. // "hello"
  44. // "world"
  45. function *gen () {
  46. yield 1;
  47. yield 2;
  48. return 3
  49. }
  50. [...gen()]
  51. // 1 2

上面代码中,Symbol.iterator方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。

遍历器对象的 return(),throw()

遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。
return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。

  1. function readLinesSync(file) {
  2. return {
  3. [Symbol.iterator]() {
  4. return {
  5. next() {
  6. return { done: false };
  7. },
  8. return() {
  9. file.close();
  10. return { done: true };
  11. }
  12. };
  13. },
  14. };
  15. }

上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其r中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。

  1. // 情况一
  2. for (let line of readLinesSync(fileName)) {
  3. console.log(line);
  4. break;
  5. }
  6. // 情况二
  7. for (let line of readLinesSync(fileName)) {
  8. console.log(line);
  9. throw new Error();
  10. }

上面代码中,情况一输出文件的第一行以后,就会执行return方法,关闭这个文件;情况二会在执行return方法关闭文件之后,再抛出错误。
注意,return方法必须返回一个对象,这是 Generator 规格决定的。
throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。