谈一下什么是可迭代协议

81 阅读5分钟

前言

近期看到一个问题就是,如何来遍历一个对象,以及对象是否可以进行遍历。谈到这个我第一想法是当然可以遍历,比如使用 for in或者Object.keys``Object.values都可以对对象进行遍历取值呀,但是却是不对的,要实现像数组一样,是可以直接通过for或者是for of这种方式来进行遍历的,我通过查询发现了可遍历的关键因素

可迭代协议

我们可以先看一些对于 for of的解释。for of语句执行一个循环,该循环处理来自可迭代对象的值序列。那么什么是可迭代对象呢,可迭代对象是指那些实现了迭代协议的对象。可迭代协议要求对象具有一个 Symbol.iterator 属性,该属性是一个函数,当调用时返回一个对象,该对象遵循迭代器协议。这样的对象可以在 for...of 循环中被遍历,切会返回迭代器获得的迭代值,我们可以打印一下数组,看一下是否存在Symbol.iterator

image.png

现在我们就知道了如何让对象进行遍历。大致可以分为,在属性上增加一个Symbol.iterator,且在调用时返回一个遵循迭代协议的对象。

迭代器协议

迭代器协议是一个定义对象如何按需产生值的规范,只有实现了特定的语法,一个对象才可以成为迭代器,迭代器对象必须实现一个 next()方法。这个方法不接受参数(或接受可选的参数),并返回一个对象,返回的对象内必须存在两个属性,done表示是否迭代完成,如果迭代完成则返回true反之则返回false,value表示当前迭代的值,当donetrue时也就是迭代完成的时候,value可以省略

const myIterator = {
  current: 0,
  last: 5,
  next() {
      if (this.current < this.last) {
          return { done: false, value: this.current++ };
      } else {
          return { done: true };
      }
  }
};

上面便是一个符合迭代器协议的对象,他有一个next()方法,并且会返回当前的值以及是否遍历完成。

对象实现迭代

我们上面已经了解了迭代器协议和可迭代协议,那么我们就可以实现对象的遍历了,只需要在对象上添加一个Symbol.iterator 属性并且返回上方的可迭代协议就可以实现

const myIterable = {
  [Symbol.iterator]() {
      let current = 0;
      const last = 5;
      return {
          next() {
              if (current < last) {
                  return { done: false, value: current++ };
              } else {
                  return { done: true };
              }
          }
      };
  }
};

for (const value of myIterable) {
  console.log(value);  // 0, 1, 2, 3, 4
}

这样我们便实现了对象的遍历

对象迭代的优化

上面可以实现对象使用迭代,但是我们的对象都是有值的,那么如何遍历的时候返回的对象是我们想要拿到的对象内的值呢,而且我们可以直接使用生成器函数搭配yield来使用

const myObject = {
  a: '1aa',
  b: '2cc',
  c: '3dd',
  d: '4ee',
  *[Symbol.iterator]() {
      for (const key in this) {
          if (this.hasOwnProperty(key)) {
              yield this[key];
          }
      }
  }
};

我们改为使用生成器函数内部使用yield来实现,yield 是生成器函数的一部分,用于在函数执行过程中生成一个序列的值,并且可以在生成值后暂停函数的执行,但不会终止函数的执行,并且可以使用next()方法,每次调用next()方法函数就会继续执行,直到遇到下一个yield暂停。

迭代器的使用场景

上面我们虽然对对象实现了可迭代,但是这种情况一般不会出现在开发中,并且在开发中完全可以使用Object.keys或者Object.values来实现,对于对象实现可迭代一般都是出现在面试场景中,但是日常开发中确实有时候也会遇到使用可迭代协议的地方例如:

自定义数据结构的遍历

比如你实现了一个复杂的数据结构(如树、图、链表等),希望能够通过 for...of 循环来遍历这个数据结构的元素。在这种情况下,实现可迭代协议就非常有用。例如,一个二叉树的数据结构:

class TreeNode {
  constructor(value) {
      this.value = value;
      this.left = null;
      this.right = null;
  }

  *inOrderTraversal() {
      if (this.left) yield* this.left.inOrderTraversal();
      yield this.value;
      if (this.right) yield* this.right.inOrderTraversal();
  }

  [Symbol.iterator]() {
      return this.inOrderTraversal();
  }
}

// 使用示例
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

for (const value of root) {
  console.log(value); // 输出:4, 2, 5, 1, 3
}

创建延迟计算的序列

比如你可能希望创建一个不立即计算出所有元素的序列,而是按需生成元素。例如,生成一个无限的斐波那契数列

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
      yield curr;
      [prev, curr] = [curr, prev + curr];
  }
}

// 使用示例
const sequence = fibonacci();
console.log(sequence.next().value); // 输出:1
console.log(sequence.next().value); // 输出:1
console.log(sequence.next().value); // 输出:2
console.log(sequence.next().value); // 输出:3
console.log(sequence.next().value); // 输出:5

这里,生成器函数 fibonacci 实现了迭代器协议,并且通过 for...ofnext() 方法可以按需获取斐波那契数列的下一个值,而不需要一次性计算出所有值

结尾

这里呢我们通过如何来遍历对象了解了可迭代协议以及迭代器协议,其实把对象变为可迭代实际上是在日常开发中基本上遇不到的,而且我们有更好的api来实现迭代对象拿到值,这里的目的主要是了解迭代器协议,来应对面试或是日常开发中可以更简便实现一些效果的方式。