← JavaScript/Перебираемые объекты#94 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Перебираемые объекты (Iterables)

Какую проблему решают итерируемые объекты

Ты пишешь класс PaginatedList — список товаров с пагинацией. Хочешь, чтобы код мог написать for (const product of list) — точно так же как с обычным массивом. Или ваша доменная модель DateRange должна поддерживать деструктуризацию и spread-оператор.

Итерируемый протокол позволяет любому объекту работать в for...of, деструктуризации, spread и Array.from.

На основе предыдущих уроков

  • «Массивы» — массив это встроенный итерируемый объект
  • «Map/Set» — Map и Set тоже итерируемые
  • «Деструктуризация» — работает с любым итерируемым
  • Встроенные итерируемые объекты

    // Строки
    for (const char of 'Привет') console.log(char)  // П, р, и, в, е, т
    
    // Map
    const prices = new Map([['apple', 120], ['banana', 80]])
    for (const [product, price] of prices) {
      console.log(product, price)
    }
    
    // Set
    for (const id of new Set([1, 2, 2, 3])) {
      console.log(id)  // 1, 2, 3 — дубли убраны
    }

    Итерируемый протокол

    Чтобы объект стал итерируемым, нужно добавить метод [Symbol.iterator](), возвращающий итератор — объект с методом next():

    const range = {
      from: 1,
      to: 5,
      [Symbol.iterator]() {
        let current = this.from
        const last = this.to
        return {
          next() {
            if (current <= last) {
              return { value: current++, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    }
    
    for (const n of range) console.log(n)  // 1, 2, 3, 4, 5
    console.log([...range])                // [1, 2, 3, 4, 5]
    const [first, , third] = range         // first=1, third=3

    Array.from и spread с итерируемыми

    // Оба принимают любой итерируемый объект:
    Array.from('hello')              // ['h', 'e', 'l', 'l', 'o']
    Array.from(new Set([1, 2, 3]))   // [1, 2, 3]
    [...new Map([['a', 1]])]        // [['a', 1]]
    Array.from(range)                // [1, 2, 3, 4, 5]
    
    // Array.from с трансформацией:
    Array.from({ length: 5 }, (_, i) => i * 2)  // [0, 2, 4, 6, 8]

    Класс-итерируемый

    class Range {
      constructor(start, end, step = 1) {
        this.start = start
        this.end = end
        this.step = step
      }
    
      [Symbol.iterator]() {
        let current = this.start
        const { end, step } = this
        return {
          next() {
            if (current <= end) {
              const value = current
              current += step
              return { value, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    }
    
    const evens = new Range(0, 10, 2)
    console.log([...evens])  // [0, 2, 4, 6, 8, 10]

    Iterable vs Array-like

    Два разных протокола, часто путают:

    | | Iterable | Array-like |

    |---|---|---|

    | Признак | Symbol.iterator | length + числовые ключи |

    | Работает в for...of | Да | Нет |

    | Примеры | Array, Map, Set, строки | arguments, DOM NodeList |

    Array.from принимает оба вида.

    Типичные ошибки

    1. Итерируют обычный объект через for...of:

    // Сломано — обычный объект не итерируемый:
    const obj = { a: 1, b: 2 }
    for (const v of obj) { ... }  // TypeError: obj is not iterable
    
    // Исправлено — итерируй по значениям:
    for (const v of Object.values(obj)) { ... }  // 1, 2

    2. Забывают что next() должен возвращать { value, done }:

    // Сломано — неверный формат:
    return {
      next() {
        return current  // просто число — ошибка!
      }
    }
    
    // Исправлено:
    return {
      next() {
        return { value: current++, done: false }
      }
    }

    3. Не возвращают { value: undefined, done: true } при завершении:

    // Сломано — бесконечный итератор:
    next() {
      return { value: current++ }  // нет done: true — никогда не остановится!
    }
    
    // Исправлено:
    next() {
      if (current > end) return { value: undefined, done: true }
      return { value: current++, done: false }
    }

    В реальных проектах

  • Генераторы (function*) — автоматически реализуют протокол итератора, более короткая запись
  • Node.js Streams: for await (const chunk of stream) — асинхронная итерация
  • Paginated API: ленивая загрузка следующей страницы при каждом next()
  • RxJS Observables: не итерируемые, но похожий принцип подписки на поток значений
  • Примеры

    Класс Range для диапазонов и итерируемый пагинатор

    // Итерируемый класс Range
    class Range {
      constructor(start, end, step = 1) {
        this.start = start
        this.end = end
        this.step = step
      }
    
      [Symbol.iterator]() {
        let current = this.start
        const { end, step } = this
        return {
          next() {
            if (current <= end) {
              const value = current
              current += step
              return { value, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    
      toArray() { return [...this] }
      size() { return Math.floor((this.end - this.start) / this.step) + 1 }
    }
    
    // Использование как обычного массива
    const r = new Range(1, 10)
    for (const n of r) process.stdout.write(n + ' ')  // 1 2 3 4 5 6 7 8 9 10
    console.log()
    
    console.log([...new Range(0, 20, 5)])  // [0, 5, 10, 15, 20]
    
    const [first, second, third] = new Range(10, 50, 10)
    console.log(first, second, third)  // 10 20 30
    
    // Итерируемый пагинатор — ленивая загрузка
    class Paginator {
      constructor(items, pageSize = 3) {
        this.items = items
        this.pageSize = pageSize
      }
    
      [Symbol.iterator]() {
        let offset = 0
        const { items, pageSize } = this
        return {
          next() {
            if (offset >= items.length) {
              return { value: undefined, done: true }
            }
            const page = items.slice(offset, offset + pageSize)
            offset += pageSize
            return { value: page, done: false }
          }
        }
      }
    }
    
    const products = ['MacBook', 'iPhone', 'AirPods', 'iPad', 'Watch', 'HomePod', 'TV']
    const paginator = new Paginator(products, 3)
    
    console.log('\nСтраницы товаров:')
    let page = 1
    for (const pageItems of paginator) {
      console.log(`Страница ${page++}:`, pageItems)
    }
    // Страница 1: ['MacBook', 'iPhone', 'AirPods']
    // Страница 2: ['iPad', 'Watch', 'HomePod']
    // Страница 3: ['TV']

    Перебираемые объекты (Iterables)

    Какую проблему решают итерируемые объекты

    Ты пишешь класс PaginatedList — список товаров с пагинацией. Хочешь, чтобы код мог написать for (const product of list) — точно так же как с обычным массивом. Или ваша доменная модель DateRange должна поддерживать деструктуризацию и spread-оператор.

    Итерируемый протокол позволяет любому объекту работать в for...of, деструктуризации, spread и Array.from.

    На основе предыдущих уроков

  • «Массивы» — массив это встроенный итерируемый объект
  • «Map/Set» — Map и Set тоже итерируемые
  • «Деструктуризация» — работает с любым итерируемым
  • Встроенные итерируемые объекты

    // Строки
    for (const char of 'Привет') console.log(char)  // П, р, и, в, е, т
    
    // Map
    const prices = new Map([['apple', 120], ['banana', 80]])
    for (const [product, price] of prices) {
      console.log(product, price)
    }
    
    // Set
    for (const id of new Set([1, 2, 2, 3])) {
      console.log(id)  // 1, 2, 3 — дубли убраны
    }

    Итерируемый протокол

    Чтобы объект стал итерируемым, нужно добавить метод [Symbol.iterator](), возвращающий итератор — объект с методом next():

    const range = {
      from: 1,
      to: 5,
      [Symbol.iterator]() {
        let current = this.from
        const last = this.to
        return {
          next() {
            if (current <= last) {
              return { value: current++, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    }
    
    for (const n of range) console.log(n)  // 1, 2, 3, 4, 5
    console.log([...range])                // [1, 2, 3, 4, 5]
    const [first, , third] = range         // first=1, third=3

    Array.from и spread с итерируемыми

    // Оба принимают любой итерируемый объект:
    Array.from('hello')              // ['h', 'e', 'l', 'l', 'o']
    Array.from(new Set([1, 2, 3]))   // [1, 2, 3]
    [...new Map([['a', 1]])]        // [['a', 1]]
    Array.from(range)                // [1, 2, 3, 4, 5]
    
    // Array.from с трансформацией:
    Array.from({ length: 5 }, (_, i) => i * 2)  // [0, 2, 4, 6, 8]

    Класс-итерируемый

    class Range {
      constructor(start, end, step = 1) {
        this.start = start
        this.end = end
        this.step = step
      }
    
      [Symbol.iterator]() {
        let current = this.start
        const { end, step } = this
        return {
          next() {
            if (current <= end) {
              const value = current
              current += step
              return { value, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    }
    
    const evens = new Range(0, 10, 2)
    console.log([...evens])  // [0, 2, 4, 6, 8, 10]

    Iterable vs Array-like

    Два разных протокола, часто путают:

    | | Iterable | Array-like |

    |---|---|---|

    | Признак | Symbol.iterator | length + числовые ключи |

    | Работает в for...of | Да | Нет |

    | Примеры | Array, Map, Set, строки | arguments, DOM NodeList |

    Array.from принимает оба вида.

    Типичные ошибки

    1. Итерируют обычный объект через for...of:

    // Сломано — обычный объект не итерируемый:
    const obj = { a: 1, b: 2 }
    for (const v of obj) { ... }  // TypeError: obj is not iterable
    
    // Исправлено — итерируй по значениям:
    for (const v of Object.values(obj)) { ... }  // 1, 2

    2. Забывают что next() должен возвращать { value, done }:

    // Сломано — неверный формат:
    return {
      next() {
        return current  // просто число — ошибка!
      }
    }
    
    // Исправлено:
    return {
      next() {
        return { value: current++, done: false }
      }
    }

    3. Не возвращают { value: undefined, done: true } при завершении:

    // Сломано — бесконечный итератор:
    next() {
      return { value: current++ }  // нет done: true — никогда не остановится!
    }
    
    // Исправлено:
    next() {
      if (current > end) return { value: undefined, done: true }
      return { value: current++, done: false }
    }

    В реальных проектах

  • Генераторы (function*) — автоматически реализуют протокол итератора, более короткая запись
  • Node.js Streams: for await (const chunk of stream) — асинхронная итерация
  • Paginated API: ленивая загрузка следующей страницы при каждом next()
  • RxJS Observables: не итерируемые, но похожий принцип подписки на поток значений
  • Примеры

    Класс Range для диапазонов и итерируемый пагинатор

    // Итерируемый класс Range
    class Range {
      constructor(start, end, step = 1) {
        this.start = start
        this.end = end
        this.step = step
      }
    
      [Symbol.iterator]() {
        let current = this.start
        const { end, step } = this
        return {
          next() {
            if (current <= end) {
              const value = current
              current += step
              return { value, done: false }
            }
            return { value: undefined, done: true }
          }
        }
      }
    
      toArray() { return [...this] }
      size() { return Math.floor((this.end - this.start) / this.step) + 1 }
    }
    
    // Использование как обычного массива
    const r = new Range(1, 10)
    for (const n of r) process.stdout.write(n + ' ')  // 1 2 3 4 5 6 7 8 9 10
    console.log()
    
    console.log([...new Range(0, 20, 5)])  // [0, 5, 10, 15, 20]
    
    const [first, second, third] = new Range(10, 50, 10)
    console.log(first, second, third)  // 10 20 30
    
    // Итерируемый пагинатор — ленивая загрузка
    class Paginator {
      constructor(items, pageSize = 3) {
        this.items = items
        this.pageSize = pageSize
      }
    
      [Symbol.iterator]() {
        let offset = 0
        const { items, pageSize } = this
        return {
          next() {
            if (offset >= items.length) {
              return { value: undefined, done: true }
            }
            const page = items.slice(offset, offset + pageSize)
            offset += pageSize
            return { value: page, done: false }
          }
        }
      }
    }
    
    const products = ['MacBook', 'iPhone', 'AirPods', 'iPad', 'Watch', 'HomePod', 'TV']
    const paginator = new Paginator(products, 3)
    
    console.log('\nСтраницы товаров:')
    let page = 1
    for (const pageItems of paginator) {
      console.log(`Страница ${page++}:`, pageItems)
    }
    // Страница 1: ['MacBook', 'iPhone', 'AirPods']
    // Страница 2: ['iPad', 'Watch', 'HomePod']
    // Страница 3: ['TV']

    Задание

    Ты разрабатываешь генератор временных слотов для системы бронирования (как Calendly). Создай итерируемый класс `TimeSlots`, который генерирует временные слоты для рабочего дня. Конструктор принимает: - `startHour` — начало рабочего дня (например, 9) - `endHour` — конец (например, 18) - `slotMinutes` — длительность слота в минутах (например, 30) Каждая итерация возвращает строку вида `"09:00"`, `"09:30"`, `"10:00"`, ... Слот `endHour:00` не включается.

    Подсказка

    Форматирование: String(hours).padStart(2, "0") + ":" + String(mins).padStart(2, "0"). currentMinutes начинается с startHour * 60, увеличивается на slotMinutes, заканчивается при >= endHour * 60.

    Загружаем среду выполнения...
    Загружаем AI-помощника...