← Назад к вопросам

Можно ли создать объект который умеет работать с for…of?

1.7 Middle🔥 141 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Итерируемые объекты и for...of в JavaScript

Да, полностью можно создать объект, который работает с for...of. Для этого нужно реализовать протокол итерируемости (Iterable Protocol) через специальный метод Symbol.iterator.

1. Понимание итерируемости

// Встроенные итерируемые объекты (работают с for...of)
const array = [1, 2, 3];
for (const item of array) {
  console.log(item); // 1, 2, 3
}

// Строки (итерируемые)
const str = "hello";
for (const char of str) {
  console.log(char); // h, e, l, l, o
}

// Map (итерируемая)
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
  console.log(key, value); // a 1, b 2
}

// Обычный объект НЕ итерируемый
const obj = { a: 1, b: 2 };
for (const item of obj) {
  // TypeError: obj is not iterable
}

2. Реализация Symbol.iterator для пользовательского объекта

// Простой класс с итерируемостью
class CountUp {
  constructor(max) {
    this.max = max;
  }

  // Реализуем Iterable Protocol
  [Symbol.iterator]() {
    let count = 0;
    const max = this.max;

    return {
      // Этот объект должен иметь метод next()
      next() {
        count++;
        if (count <= max) {
          return { value: count, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

// Использование
const counter = new CountUp(3);
for (const num of counter) {
  console.log(num); // 1, 2, 3
}

// Также работает со spread оператором
const array = [...counter]; // [1, 2, 3]

// И с деструктуризацией
const [first, second, third] = counter; // first=1, second=2, third=3

3. Более сложный пример: итерируемый класс

// Класс для хранения пользователей с итерируемостью
class UserCollection {
  private users: Array<{ id: number; name: string }> = [];

  addUser(name: string) {
    this.users.push({
      id: this.users.length + 1,
      name
    });
  }

  // Реализуем итерируемость
  [Symbol.iterator]() {
    let index = 0;
    const users = this.users;

    return {
      next() {
        if (index < users.length) {
          return {
            value: users[index++],
            done: false
          };
        }
        return { done: true };
      }
    };
  }
}

// Использование
const users = new UserCollection();
users.addUser("Alice");
users.addUser("Bob");
users.addUser("Charlie");

for (const user of users) {
  console.log(user.name); // Alice, Bob, Charlie
}

// Работает с методами, которые ожидают итерируемые объекты
const userArray = Array.from(users); // Преобразует в массив
const names = [...users].map(u => u.name); // Spread оператор

4. Использование Generator для упрощения

// Generator функция - самый простой способ создать итератор
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    // function* = generator function
    // yield = вернуть значение и паузировать
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

// Использование
const range = new Range(1, 5);
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Generator намного проще чем вручную создавать { next() }

5. Итерируемые паттерны

// ПАТТЕРН 1: Простой счётчик (как выше)

// ПАТТЕРН 2: Фильтрованная коллекция
class FilteredList {
  constructor(items, predicate) {
    this.items = items;
    this.predicate = predicate;
  }

  *[Symbol.iterator]() {
    for (const item of this.items) {
      if (this.predicate(item)) {
        yield item;
      }
    }
  }
}

const numbers = [1, 2, 3, 4, 5];
const evens = new FilteredList(numbers, n => n % 2 === 0);
for (const num of evens) {
  console.log(num); // 2, 4
}

// ПАТТЕРН 3: Трансформирующая коллекция
class MappedList {
  constructor(items, transform) {
    this.items = items;
    this.transform = transform;
  }

  *[Symbol.iterator]() {
    for (const item of this.items) {
      yield this.transform(item);
    }
  }
}

const squared = new MappedList([1, 2, 3], x => x * x);
for (const num of squared) {
  console.log(num); // 1, 4, 9
}

// ПАТТЕРН 4: Бесконечная последовательность
class FibonacciSequence {
  *[Symbol.iterator]() {
    let a = 0, b = 1;
    while (true) {
      yield a;
      [a, b] = [b, a + b];
    }
  }
}

const fib = new FibonacciSequence();
let count = 0;
for (const num of fib) {
  console.log(num);
  if (++count >= 10) break; // Нужно остановиться вручную
}

6. Практический пример: API ответы

// Класс для работы с API, который поддерживает пагинацию
class PaginatedAPI {
  private baseURL: string;
  private pageSize: number;

  constructor(baseURL: string, pageSize: number = 10) {
    this.baseURL = baseURL;
    this.pageSize = pageSize;
  }

  // Работает с for...of
  async *[Symbol.asyncIterator]() {
    let page = 1;

    while (true) {
      const response = await fetch(
        `${this.baseURL}?page=${page}&limit=${this.pageSize}`
      );
      const data = await response.json();

      if (data.items.length === 0) break; // Конец данных

      for (const item of data.items) {
        yield item; // yield из async generator
      }

      if (!data.hasNextPage) break;
      page++;
    }
  }
}

// Использование с async for...of
async function fetchAll() {
  const api = new PaginatedAPI("https://api.example.com/items");

  for await (const item of api) {
    // await нужен для async iterator
    console.log(item);
  }
}

7. Сравнение способов итерации

const data = { a: 1, b: 2, c: 3 };

// for...in (для обычных объектов) - НЕ рекомендуется
for (const key in data) {
  console.log(key); // a, b, c (ключи)
  console.log(data[key]); // 1, 2, 3
}

// Object.keys/values/entries (для обычных объектов)
for (const key of Object.keys(data)) {
  console.log(key); // a, b, c
}

for (const value of Object.values(data)) {
  console.log(value); // 1, 2, 3
}

for (const [key, value] of Object.entries(data)) {
  console.log(key, value); // a 1, b 2, c 3
}

// for...of (для итерируемых объектов)
class IterableObject {
  constructor(data) {
    this.data = data;
  }

  *[Symbol.iterator]() {
    for (const [key, value] of Object.entries(this.data)) {
      yield { key, value };
    }
  }
}

const iterable = new IterableObject(data);
for (const item of iterable) {
  console.log(item.key, item.value); // a 1, b 2, c 3
}

8. Проверка итерируемости

// Проверить, является ли объект итерируемым
function isIterable(obj) {
  if (obj == null) return false;
  return typeof obj[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3])); // true (массив)
console.log(isIterable("hello")); // true (строка)
console.log(isIterable(new Map())); // true (map)
console.log(isIterable(new Set())); // true (set)
console.log(isIterable({ a: 1 })); // false (обычный объект)

// Пользовательский объект с iterator
class MyIterable {
  *[Symbol.iterator]() {
    yield 1;
  }
}

console.log(isIterable(new MyIterable())); // true

// Безопасно использовать
const obj = new MyIterable();
if (isIterable(obj)) {
  for (const item of obj) {
    console.log(item); // 1
  }
}

Рекомендация

Для создания итерируемого объекта:

  1. Реализуй Symbol.iterator — вернёт объект с методом next()
  2. Используй Generator (*) — самый простой способ
  3. Для асинхронных данных — реализуй Symbol.asyncIterator с async *
  4. Проверяй итерируемостьSymbol.iterator in obj
  5. Практические случаи: коллекции, пагинированные API, фильтры, трансформации