Skip to content

迭代协议:for...of 背后的隐藏接口

"JavaScript 中的 for...of 循环看起来简单直接,但其背后隐藏着一套精巧的迭代协议。理解这套协议不仅能帮助我们更好地使用迭代功能,还能让我们创建自定义的可迭代对象。"

迭代是编程中的基本操作之一,在 JavaScript 中,ES6 引入了 for...of 循环和迭代协议,为遍历各种数据结构提供了统一的机制。这套协议不仅适用于内置的数组、字符串等类型,还允许我们为自定义对象定义迭代行为。

一、迭代协议基础概念

JavaScript 中有两种相关的协议:

  1. 可迭代协议(Iterable Protocol) - 允许对象定义自己的迭代行为
  2. 迭代器协议(Iterator Protocol) - 定义了产生一系列值的标准方式

可迭代协议

要成为可迭代对象,对象必须实现 @@iterator 方法,这意味着对象(或其原型链上的对象)必须有一个键为 Symbol.iterator 的属性,其值是一个无参数的函数,该函数返回一个符合迭代器协议的对象。

javascript
const iterable = {
  [Symbol.iterator]() {
    // 返回一个迭代器对象
    return iterator;
  }
};

迭代器协议

迭代器对象必须实现 next() 方法,该方法返回一个具有以下两个属性的对象:

  • value - 序列中的下一个值
  • done - 布尔值,表示迭代是否完成
javascript
const iterator = {
  next() {
    return {
      value: 'some value',
      done: false // 或 true 表示完成
    };
  }
};

二、内置可迭代对象

JavaScript 的许多内置对象都是可迭代的:

javascript
// 数组
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value); // 1, 2, 3
}

// 字符串
const str = "abc";
for (const char of str) {
  console.log(char); // "a", "b", "c"
}

// Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(key, value); // "a" 1, "b" 2
}

// Set
const set = new Set([1, 2, 3]);
for (const value of set) {
  console.log(value); // 1, 2, 3
}

// arguments 对象
function example() {
  for (const arg of arguments) {
    console.log(arg);
  }
}
example(1, 2, 3); // 1, 2, 3

// NodeList
// for (const node of document.querySelectorAll('div')) {
//   console.log(node);
// }

三、手动实现迭代协议

让我们手动实现一个简单的可迭代对象:

javascript
const iterableObj = {
  data: [1, 2, 3, 4, 5],
  
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    
    return {
      next() {
        if (index < data.length) {
          return {
            value: data[index++],
            done: false
          };
        } else {
          return {
            done: true
          };
        }
      }
    };
  }
};

// 使用 for...of 遍历
for (const value of iterableObj) {
  console.log(value); // 1, 2, 3, 4, 5
}

// 使用扩展运算符
console.log([...iterableObj]); // [1, 2, 3, 4, 5]

// 使用 Array.from
console.log(Array.from(iterableObj)); // [1, 2, 3, 4, 5]

四、使用生成器简化迭代器实现

生成器函数可以大大简化迭代器的实现:

javascript
const iterableObjWithGenerator = {
  data: [1, 2, 3, 4, 5],
  
  *[Symbol.iterator]() {
    for (const item of this.data) {
      yield item;
    }
  }
};

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

五、创建自定义可迭代对象

1. 范围迭代器

javascript
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }
  
  [Symbol.iterator]() {
    let current = this.start;
    const step = this.step;
    const end = this.end;
    
    return {
      next() {
        if (current < end) {
          const value = current;
          current += step;
          return { value, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

const range = new Range(1, 10, 2);
for (const num of range) {
  console.log(num); // 1, 3, 5, 7, 9
}

console.log([...range]); // [1, 3, 5, 7, 9]

2. 无限序列迭代器

javascript
class FibonacciSequence {
  [Symbol.iterator]() {
    let prev = 0;
    let curr = 1;
    
    return {
      next() {
        const value = prev;
        [prev, curr] = [curr, prev + curr];
        return { value, done: false };
      }
    };
  }
}

const fibonacci = new FibonacciSequence();
const fibIterator = fibonacci[Symbol.iterator]();

// 获取前10个斐波那契数
for (let i = 0; i < 10; i++) {
  console.log(fibIterator.next().value);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

3. 可中断的迭代器

javascript
class Countdown {
  constructor(from) {
    this.from = from;
  }
  
  [Symbol.iterator]() {
    let current = this.from;
    let cancelled = false;
    
    const iterator = {
      next() {
        if (cancelled || current < 0) {
          return { done: true };
        }
        return {
          value: current--,
          done: false
        };
      },
      
      cancel() {
        cancelled = true;
      }
    };
    
    return iterator;
  }
}

const countdown = new Countdown(5);
const iterator = countdown[Symbol.iterator]();

console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 4, done: false }
iterator.cancel(); // 取消迭代
console.log(iterator.next()); // { done: true }

六、迭代协议的实际应用

1. 数据结构的自定义遍历

javascript
class TreeNode {
  constructor(value) {
    this.value = value;
    this.children = [];
  }
  
  addChild(child) {
    this.children.push(child);
  }
  
  *[Symbol.iterator]() {
    // 深度优先遍历
    yield this.value;
    for (const child of this.children) {
      yield* child;
    }
  }
  
  *breadthFirst() {
    // 广度优先遍历
    const queue = [this];
    while (queue.length > 0) {
      const node = queue.shift();
      yield node.value;
      queue.push(...node.children);
    }
  }
}

// 创建树结构
const root = new TreeNode('root');
const child1 = new TreeNode('child1');
const child2 = new TreeNode('child2');
const grandchild1 = new TreeNode('grandchild1');

root.addChild(child1);
root.addChild(child2);
child1.addChild(grandchild1);

// 深度优先遍历
console.log([...root]); // ["root", "child1", "grandchild1", "child2"]

// 广度优先遍历
console.log([...root.breadthFirst()]); // ["root", "child1", "child2", "grandchild1"]

2. 数据流处理

javascript
class DataStream {
  constructor(data) {
    this.data = data;
  }
  
  *[Symbol.iterator]() {
    for (const item of this.data) {
      // 模拟数据处理
      yield item.toUpperCase();
    }
  }
  
  *filter(predicate) {
    for (const item of this) {
      if (predicate(item)) {
        yield item;
      }
    }
  }
  
  *map(transform) {
    for (const item of this) {
      yield transform(item);
    }
  }
}

const stream = new DataStream(['hello', 'world', 'javascript']);
const processed = stream
  .map(str => str.toUpperCase())
  .filter(str => str.length > 5);

console.log([...processed]); // ["JAVASCRIPT"]

七、与其他遍历方法的对比

javascript
const data = [1, 2, 3, 4, 5];

// for...of 循环
for (const item of data) {
  console.log(item);
}

// forEach 方法
data.forEach(item => console.log(item));

// for 循环
for (let i = 0; i < data.length; i++) {
  console.log(data[i]);
}

// for...in 循环(不推荐用于数组)
for (const key in data) {
  console.log(data[key]);
}

for...of 的优势:

  1. 语法简洁
  2. 只遍历可枚举属性
  3. 适用于所有可迭代对象
  4. 可以使用 break、continue 和 return

八、错误处理和边界情况

javascript
const brokenIterable = {
  [Symbol.iterator]() {
    let count = 0;
    return {
      next() {
        if (count++ < 3) {
          return { value: count, done: false };
        } else {
          throw new Error('迭代器出错');
        }
      }
    };
  }
};

try {
  for (const value of brokenIterable) {
    console.log(value);
  }
} catch (e) {
  console.log('捕获到错误:', e.message); // 捕获到错误: 迭代器出错
}

一句话总结

JavaScript 的迭代协议通过 Symbol.iterator 和 next() 方法为各种数据结构提供了统一的遍历机制,不仅适用于内置对象,还可以通过自定义实现让任何对象支持 for...of 循环和扩展运算符等语法。

理解迭代协议有助于我们更好地使用 JavaScript 的现代特性,创建更灵活的数据结构,并实现复杂的迭代逻辑。通过生成器函数,我们可以更简洁地实现迭代器,使代码更加清晰易读。