← JavaScript/Перебираемые объекты#94 из 430← ПредыдущийСледующий →+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

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

    IterableArray-like
    ПризнакSymbol.iteratorlength + числовые ключи
    Работает в 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

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

    IterableArray-like
    ПризнакSymbol.iteratorlength + числовые ключи
    Работает в 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-помощника...