JavaScript 为对象添加自定义的迭代器语法

梗概

JavaScript中,可以通过实现Symbol.iterator方法来为任何对象添加自定义的迭代规则,使其成为可迭代对象,从而能够使用for...of循环、展开运算符和解构赋值等特性。

基本语法

const myObject = {
  // 对象的其他属性和方法
  ...
  
  // 添加Symbol.iterator方法
  [Symbol.iterator]() {
    // 返回一个迭代器对象
    return {
      // 迭代器必须具有next方法
      next() {
        // next方法必须返回包含value和done属性的对象
        return { value: 某个值, done: 布尔值 };
      }
    };
  }
};

迭代器协议

一个标准的迭代器必须符合以下要求:

  1. 迭代器必须是一个对象
  2. 迭代器对象必须有一个next()方法
  3. next()方法必须返回一个对象,包含两个属性:
    • value: 当前迭代值
    • done: 布尔值,表示迭代是否结束

详细示例

示例1:为普通对象添加迭代器

const person = {
  name: 'John',
  age: 30,
  skills: ['JavaScript', 'React', 'Node.js'],
  
  [Symbol.iterator]() {
    // 要迭代的属性数组
    const properties = Object.keys(this);
    let index = 0;
    
    // 返回迭代器对象
    return {
      next: () => {
        if (index < properties.length) {
          const key = properties[index++];
          // 跳过Symbol.iterator方法
          if (key === 'Symbol(Symbol.iterator)') {
            return this.next();
          }
          return { 
            value: [key, this[key]], 
            done: false 
          };
        } else {
          return { done: true };
        }
      }
    };
  }
};
 
// 使用for...of循环遍历
for (const [key, value] of person) {
  console.log(`${key}: ${value}`);
}
// 输出:
// name: John
// age: 30
// skills: JavaScript,React,Node.js

示例2:创建自定义范围迭代器

function range(start, end, step = 1) {
  return {
    // 让函数返回的对象具有迭代器
    [Symbol.iterator]() {
      let current = start;
      
      return {
        next() {
          if (current <= end) {
            const value = current;
            current += step;
            return { value, done: false };
          } else {
            return { done: true };
          }
        }
      };
    }
  };
}
 
// 使用自定义范围迭代器
for (const num of range(1, 10, 2)) {
  console.log(num); // 1, 3, 5, 7, 9
}
 
// 结合展开运算符
const numbers = [...range(1, 5)]; // [1, 2, 3, 4, 5]

示例3:为类添加迭代器

class Collection {
  constructor(items = []) {
    this.items = items;
  }
  
  add(item) {
    this.items.push(item);
  }
  
  // 添加迭代器方法
  [Symbol.iterator]() {
    let index = 0;
    
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}
 
const collection = new Collection(['a', 'b', 'c']);
collection.add('d');
 
// 使用for...of遍历
for (const item of collection) {
  console.log(item); // 'a', 'b', 'c', 'd'
}
 
// 结合解构赋值
const [first, ...rest] = collection;
console.log(first); // 'a'
console.log(rest);  // ['b', 'c', 'd']

可重用的迭代器

通常,迭代器是不可重用的,一旦迭代完成就不能再次迭代。如果需要可重用的迭代器,可以使迭代器自身也实现可迭代协议:

const repeatable = {
  data: [1, 2, 3],
  
  [Symbol.iterator]() {
    let index = 0;
    
    // 迭代器自身也实现Symbol.iterator方法
    return {
      // next方法
      next() {
        if (index < repeatable.data.length) {
          return { value: repeatable.data[index++], done: false };
        } else {
          return { done: true };
        }
      },
      
      // 迭代器本身也是可迭代的
      [Symbol.iterator]() {
        return this;
      }
    };
  }
};
 
const iterator = repeatable[Symbol.iterator]();
 
// 第一次迭代
for (const item of iterator) {
  console.log(item); // 1, 2, 3
}
 
// 重置索引
index = 0;
 
// 第二次迭代(不可能,因为迭代器已经用完了)
for (const item of iterator) {
  console.log(item); // 不会输出任何内容
}

异步迭代器

ES2018引入了异步迭代器,使用Symbol.asyncIterator而不是Symbol.iterator,并结合for await...of循环:

const asyncIterable = {
  data: [1, 2, 3],
  
  [Symbol.asyncIterator]() {
    let index = 0;
    
    return {
      next() {
        return new Promise((resolve) => {
          setTimeout(() => {
            if (index < asyncIterable.data.length) {
              resolve({ value: asyncIterable.data[index++], done: false });
            } else {
              resolve({ done: true });
            }
          }, 1000);
        });
      }
    };
  }
};
 
// 使用异步迭代器
async function iterate() {
  for await (const item of asyncIterable) {
    console.log(item); // 每隔1秒依次输出: 1, 2, 3
  }
}
 
iterate();

注意事项

  1. 迭代器通常是一次性使用的,迭代完成后就不能再次使用,除非特意设计为可重用迭代器
  2. 迭代器的next()方法可以接受参数,用于向迭代器传递值(例如在Generator中)
  3. 在实现迭代器时,要注意闭包和this绑定问题,建议使用箭头函数保持正确的this引用

相关链接