JS `Iterable` (可迭代) 协议:自定义对象的 `for…of` 行为

各位观众老爷,晚上好!今天咱们聊聊JavaScript里一个挺有意思的东西:Iterable 协议。这玩意儿说白了,就是让你自己定义的 JavaScript 对象也能像数组一样,用 for...of 循环来遍历。听起来是不是挺酷炫的?

咱们先打个预防针,这玩意儿初学可能会觉得有点绕,但只要你跟着我一步一步走,保证你能掌握。而且,掌握了它,你在 JavaScript 的江湖地位,绝对能提升一个档次。

开场白:for...of 的诱惑

在 JavaScript 的世界里,我们经常需要遍历一些数据结构。比如数组:

const myArray = [1, 2, 3, 4, 5];

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

这个 for...of 循环用起来是不是感觉特别舒服?简洁明了,比传统的 for 循环和 forEach 方便多了。

但是,如果你想让自己的对象也能用 for...of 循环,那就需要用到今天的主角:Iterable 协议了。

什么是 Iterable 协议?

Iterable 协议其实很简单,它规定了一个对象要能够被 for...of 循环遍历,必须满足两个条件:

  1. 必须有一个 Symbol.iterator 属性。 这个属性的值是一个函数,这个函数返回一个迭代器(Iterator)对象。

  2. 迭代器对象必须有一个 next() 方法。 这个 next() 方法返回一个对象,这个对象包含两个属性:valuedone

    • value:表示当前迭代的值。
    • done:表示迭代是否结束。如果迭代结束,done 的值为 true,否则为 false

是不是有点抽象?别怕,咱们用一个表格来总结一下:

协议 说明
Iterable 定义了对象如何被 for...of 循环遍历。
Symbol.iterator 对象的一个属性,其值是一个函数,返回一个迭代器对象。
Iterator Symbol.iterator 返回的对象,必须包含 next() 方法。
next() 迭代器对象的方法,返回一个对象,包含 valuedone 属性。
value next() 方法返回的对象的一个属性,表示当前迭代的值。
done next() 方法返回的对象的一个属性,表示迭代是否结束。 true 表示迭代结束, false 表示迭代未结束。

举个栗子:自定义一个可迭代对象

光说不练假把式,咱们来写一个例子,让大家更直观地理解 Iterable 协议。

假设我们有一个 Range 类,表示一个数字范围。我们希望能够用 for...of 循环来遍历这个范围内的所有数字。

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // 避免 `this` 指向问题

    return {
      next() {
        if (currentValue <= that.end) {
          return { value: currentValue++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

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

咱们来一步一步分析一下这段代码:

  1. [Symbol.iterator]() 方法: 这是实现 Iterable 协议的关键。这个方法返回一个迭代器对象。

  2. 迭代器对象: 这个对象包含一个 next() 方法。

  3. next() 方法: 这个方法返回一个对象,包含 valuedone 属性。

    • value:表示当前迭代的值,也就是 currentValue
    • done:表示迭代是否结束。当 currentValue 大于 this.end 时,迭代结束,done 的值为 true

更简洁的写法:使用生成器函数

上面的代码稍微有点繁琐,我们可以使用生成器函数来简化代码。生成器函数是 ES6 引入的一个新特性,它可以更方便地创建迭代器。

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

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

在这个版本中,我们使用了 * 符号来定义一个生成器函数。生成器函数使用 yield 关键字来产生值。

yield i 相当于:

return { value: i, done: false };

当循环结束时,生成器函数会自动返回:

return { value: undefined, done: true };

是不是感觉清爽多了?

Iterable 协议的应用场景

Iterable 协议的应用场景非常广泛,比如:

  • 自定义数据结构: 你可以用它来让你的自定义数据结构支持 for...of 循环。
  • 异步数据流: 你可以用它来处理异步数据流,比如从服务器获取数据。
  • 惰性计算: 你可以用它来实现惰性计算,也就是只有在需要的时候才计算值。

再来几个例子:

  1. 斐波那契数列:
class Fibonacci {
    constructor(max) {
        this.max = max;
    }

    *[Symbol.iterator]() {
        let a = 0;
        let b = 1;
        let count = 0;
        while (count < this.max) {
            yield a;
            [a, b] = [b, a + b];
            count++;
        }
    }
}

const fibonacciSequence = new Fibonacci(10);

for (const number of fibonacciSequence) {
    console.log(number); // 输出斐波那契数列的前10项
}
  1. 无限循环的字母表:
class Alphabet {
    *[Symbol.iterator]() {
        let charCode = 'A'.charCodeAt(0);
        while (true) {
            yield String.fromCharCode(charCode);
            charCode++;
            if (charCode > 'Z'.charCodeAt(0)) {
                charCode = 'A'.charCodeAt(0);
            }
        }
    }
}

const alphabet = new Alphabet();
let count = 0;
for (const letter of alphabet) {
    console.log(letter);
    count++;
    if (count > 25) { // 限制循环次数
        break;
    }
}

Iterable 协议与 Iterator 协议

Iterable 协议和 Iterator 协议经常一起出现,它们是密切相关的。

  • Iterable 协议定义了如何获取一个迭代器。
  • Iterator 协议定义了迭代器应该如何工作。

一个对象要能够被 for...of 循环遍历,必须同时满足这两个协议。

内置的可迭代对象

JavaScript 内置了很多可迭代对象,比如:

  • Array
  • String
  • Map
  • Set
  • arguments 对象
  • NodeList (DOM 节点列表)

这些对象都实现了 Iterable 协议,所以可以直接用 for...of 循环来遍历。

for...in vs for...of

很多同学容易把 for...infor...of 搞混,它们之间有很大的区别:

  • for...in 循环遍历的是对象的属性名(key)。
  • for...of 循环遍历的是对象的属性值(value)。
const myObject = { a: 1, b: 2, c: 3 };

for (const key in myObject) {
  console.log(key); // 输出 "a", "b", "c"
}

// myObject 不是 Iterable 对象,直接使用 for...of 会报错
// for (const value of myObject) {
//   console.log(value);
// }

总结

Iterable 协议是 JavaScript 中一个非常重要的概念,它可以让你自定义的对象支持 for...of 循环。掌握了它,你可以写出更简洁、更优雅的代码。

咱们再用一个表格来总结一下今天的内容:

概念 说明
Iterable 协议 定义了对象如何被 for...of 循环遍历。
Symbol.iterator 对象的一个属性,其值是一个函数,返回一个迭代器对象。
Iterator 协议 定义了迭代器应该如何工作。
next() 方法 迭代器对象的方法,返回一个对象,包含 valuedone 属性。
value next() 方法返回的对象的一个属性,表示当前迭代的值。
done next() 方法返回的对象的一个属性,表示迭代是否结束。 true 表示迭代结束, false 表示迭代未结束。
生成器函数 可以更方便地创建迭代器。使用 yield 关键字来产生值。
for...in vs for...of for...in 循环遍历的是对象的属性名(key),for...of 循环遍历的是对象的属性值(value)。

希望今天的讲座对大家有所帮助。 记住,编程之路没有捷径,只有不断学习和实践,才能成为真正的编程高手。

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注