# (四)Iterator(遍历器)和for...of循环

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

# 一、迭代器和 for...of 浅谈

# 1.1 传统 for 循环

先来看一段标准的 for 循环的代码:

var arr = [1,2,3];

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}
// 1 2 3

注意,我们拿到了里面的元素,但却多做了很多事:

  • 我们声明了 i 标索引
  • 确定了边界,一旦多层嵌套;
function unique(array) {
  var res = [];
  for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
    for (var j = 0, resLen = res.length; j < resLen; j++) {
      if (array[i] === res[j]) {
        break;
      }
    }
    if (j === resLen) {
      // 把首次出现的加入到新数组中
      res.push(array[i]);
    }
  }
  return res;
}

为了消除这种复杂度以及减少循环中的错误(比如错误使用其他循环中的变量),ES6 提供了迭代器和 for of 循环共同解决这个问题。

# 1.2 iterator(迭代器)

迭代器的描述:

  • 是为各种数据结构,提供一个统一的、简便的访问接口,是用于遍历数据结构元素的指针
  • 二是使得数据结构的成员能够按某种次序排列;
  • 三是 ES6创造的一种遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

迭代的过程如下:

  1. 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置;
  2. 随后通过 next 方法进行向下迭代指向下一个位置:
    1. next 方法会返回当前位置的对象,对象包含了 valuedone 两个属性;
    2. value 是当前属性的值;
    3. done 用于判断是否遍历结束,done 为 true 时则遍历结束;

迭代的内部逻辑应该是:

var 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) {
  let index = 0;
  const iterator = {};
  iterator.next = function() {
    if (index < array.length) return { value: array[index++], done: false };
    return { value: undefined, done: true };
  };
  return iterator;
}

# 1.3 什么是 for...of?

注意这里我们仅提及了 forof 与迭代器的关系。

for...of 的描述:

  • for...of 语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句——MDN
  • 一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。

看到这里你会发现for...of和迭代器总是在一起, for...of循环内部调用的是数据结构的Symbol.iterator方法。

举个例子:

const obj = {
  value: 1,
};

for (value of obj) {
  console.log(value);
}
// TypeError: iterator is not iterable

我们直接 for of 遍历一个对象,会报错,然而如果我们给该对象添加 Symbol.iterator 属性:

const obj = {
  value: 1,
};

obj[Symbol.iterator] = function() {
  return createIterator([1, 2, 3]);
};

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

由此,我们也可以发现 for...of 遍历的其实是对象的 Symbol.iterator 属性。

JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。

var arr = ["a", "b", "c", "d"];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

上面代码表明:

  • for...in 循环读取键名
  • for...of 循环读取键值

for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟 for...in 循环也不一样。

# 二、默认的 Iterator 接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制。当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

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

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

拿数组举例:

const item = [1, 2, 3][Symbol.iterator]();
item.next();
item.next();
item.next();
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: undefined, done: true}

对于原生部署Iterator接口的数据结构,不用自己写遍历器生成函数,for...of 循环会自动遍历它们。除此之外,都需要自己在 Symbol.iterator 属性上面部署。

本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。

对象(Object)之所以没有默认部署 Iterator 接口,也是因为对象没法统一进行线性转换

一个对象如果要具备可被 for...of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

class newiterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }
  // Iterator接口 返回本身
  [Symbol.iterator]() {
    return this;
  }

  next() {
    if (this.value < this.stop) {
      return { value: this.value++, done: false };
    }
    return { value: undefined, done: true };
  }
}

const iterator = new newiterator(0, 3);

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

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

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

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

[...document.querySelectorAll("div")]; // 可以执行了

NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的 Symbol.iterator 属性,可以看到没有任何影响。

注意,普通对象部署数组的 Symbol.iterator 方法,并无效果。

let iterable = {
  a: "a",
  b: "b",
  c: "c",
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator],
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

# 三、模拟实现的 for...of

其实模拟实现 for of 也比较简单,就是利用它与 Symbol.iterator 的关系。

function forOf(obj, cb) {
  let iterable, result;

  if (typeof obj[Symbol.iterator] !== "function")
    throw new TypeError(result + " is not iterable");
  if (typeof cb !== "function") throw new TypeError("cb must be callable");

  iterable = obj[Symbol.iterator]();

  result = iterable.next();
  while (!result.done) {
    cb(result.value);
    result = iterable.next();
  }
}

# 四、使用 Iterator 接口的场景

有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法),除了 for...of 循环,还有几个别的场合。

# 4.1 解构赋值

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

let set = new Set()
  .add("a")
  .add("b")
  .add("c");

let [x, y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

# 4.2 扩展运算符

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

// 例一
var str = "hello";
[...str]; //  ['h','e','l','l','o']

// 例二
let arr = ["b", "c"];
["a", ...arr, "d"];
// ['a', 'b', 'c', 'd']

上面代码的扩展运算符内部就调用 Iterator 接口。

实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

# 4.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.4 其他场合

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

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

# 五、Iterator 接口与 Generator 函数

Symbol.iterator()方法的最简单实现,还是使用 ES6 新提出的 Generator 函数。

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
};
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

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

# 六、遍历器对象的 return(),throw()

遍历器对象除了具有 next()方法,还可以具有 return()方法和 throw()方法。如果你自己写遍历器对象生成函数,那么 next()方法是必须部署的,return()方法和 throw()方法是否部署是可选的。

return()方法的使用场合是,如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句),就会调用 return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return()方法。

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        },
      };
    },
  };
}

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

// 情况一
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 情况二
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

上面代码中:

  • 情况一输出文件的第一行以后,就会执行 return()方法,关闭这个文件;
  • 情况二会在执行 return()方法关闭文件之后,再抛出错误。

# 参考

# 写在最后

JavaScript 系列:

  1. 《JavaScript 内功进阶系列》(已完结) (opens new window)
  2. 《JavaScript 专项系列》(持续更新) (opens new window)
  3. 《ES6 基础系列》(持续更新) (opens new window)

关于我

  • 花名:余光(沉迷 JS,虚心学习中)
  • WX:j565017805

其他沉淀

这是文章所在 GitHub 仓库的传送门 (opens new window),您点的 star,就是对我最大的鼓励 ~

Last Updated: 2/15/2023, 7:45:22 PM