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

Расширение встроенных классов

Представь: ты работаешь с данными аналитики — массивами чисел, которые нужно суммировать, группировать, разбивать на страницы. Каждый раз писать array.reduce(...) неудобно. Что если массив сам знал бы .sum(), .groupBy(), .chunk()? Это и есть расширение встроенных классов — добавление доменной логики прямо в структуру данных.

Что решает этот механизм

Встроенные классы (Array, Map, Set, Error) покрывают общие операции, но ничего не знают о твоей предметной области. Наследование позволяет добавить методы конкретного домена, сохранив всю мощь стандартного API.

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

  • классы, extends, super — механизм наследования, который используется здесь
  • Symbol — Symbol.species управляет типом возвращаемых значений методов
  • Map, Set — базовые классы, которые можно расширять
  • class PowerArray extends Array

    class PowerArray extends Array {
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    
      average() {
        if (this.length === 0) return 0
        return this.sum() / this.length
      }
    
      flatten() {
        return this.reduce((acc, val) =>
          acc.concat(Array.isArray(val) ? val : [val]), new PowerArray())
      }
    }
    
    const nums = new PowerArray(10, 20, 30, 40)
    console.log(nums.sum())      // 100
    console.log(nums.average())  // 25
    console.log(nums instanceof PowerArray)  // true
    console.log(nums instanceof Array)       // true

    Symbol.species — тип возвращаемых значений

    По умолчанию методы map, filter, slice возвращают тот же класс (PowerArray), а не обычный Array:

    const nums = new PowerArray(10, 20, 30, 40, 50)
    const filtered = nums.filter(n => n > 20)  // возвращает PowerArray!
    console.log(filtered instanceof PowerArray)  // true
    console.log(filtered.sum())                  // 120 — методы доступны

    Чтобы map/filter возвращали обычный Array, используй Symbol.species:

    class PowerArray extends Array {
      static get [Symbol.species]() {
        return Array  // map/filter/slice вернут обычный Array
      }
    
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    }
    
    const nums = new PowerArray(1, 2, 3)
    const doubled = nums.map(x => x * 2)   // обычный Array
    console.log(doubled instanceof PowerArray)  // false
    console.log(doubled instanceof Array)       // true
    // doubled.sum() — ошибка! sum() нет на Array

    Без Symbol.species цепочки трансформаций сохраняют расширенный тип — удобно для fluent API.

    Расширение Map

    class DefaultMap extends Map {
      constructor(defaultValue, entries) {
        super(entries)
        this._default = defaultValue
      }
    
      get(key) {
        if (!this.has(key)) {
          this.set(key, typeof this._default === 'function'
            ? this._default(key)
            : this._default)
        }
        return super.get(key)
      }
    }
    
    const counter = new DefaultMap(0)
    counter.set('apple', counter.get('apple') + 1)  // 1 (было 0 по умолчанию)
    counter.set('apple', counter.get('apple') + 1)  // 2
    counter.get('banana')  // 0 — создаётся запись по умолчанию

    Расширение Error

    Принято создавать иерархию ошибок для точной обработки через instanceof:

    class AppError extends Error {
      constructor(message, code) {
        super(message)
        this.name = 'AppError'  // ВАЖНО: установить name вручную
        this.code = code
      }
    }
    
    class NetworkError extends AppError {
      constructor(message, statusCode) {
        super(message, 'NETWORK_ERROR')
        this.name = 'NetworkError'
        this.statusCode = statusCode
      }
    }
    
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 'VALIDATION_ERROR')
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    // Обработка по типу
    try {
      throw new ValidationError('Поле обязательно', 'email')
    } catch (err) {
      if (err instanceof ValidationError) {
        console.log(`Ошибка в поле "${err.field}": ${err.message}`)
      } else if (err instanceof NetworkError) {
        console.log(`HTTP ${err.statusCode}: ${err.message}`)
      } else {
        throw err  // пробрасываем неизвестные ошибки
      }
    }

    Реальный пример: SortedArray

    class SortedArray extends Array {
      constructor(compareFn, ...items) {
        super()
        this._compare = compareFn || ((a, b) => a > b ? 1 : a < b ? -1 : 0)
        this.insertMany(items)
      }
    
      insert(item) {
        // Бинарный поиск позиции вставки
        let lo = 0, hi = this.length
        while (lo < hi) {
          const mid = (lo + hi) >> 1
          if (this._compare(this[mid], item) <= 0) lo = mid + 1
          else hi = mid
        }
        this.splice(lo, 0, item)
        return this
      }
    
      insertMany(items) {
        items.forEach(item => this.insert(item))
        return this
      }
    }

    Реальный пример: ObservableArray

    class ObservableArray extends Array {
      constructor(...args) {
        super(...args)
        this._listeners = []
      }
    
      onChange(handler) {
        this._listeners.push(handler)
        return () => {
          this._listeners = this._listeners.filter(h => h !== handler)
        }
      }
    
      _notify(action, items) {
        this._listeners.forEach(h => h({ action, items, length: this.length }))
      }
    
      push(...items) {
        const result = super.push(...items)
        this._notify('push', items)
        return result
      }
    
      pop() {
        const item = super.pop()
        this._notify('pop', [item])
        return item
      }
    }

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

    1. Забыть установить this.name в кастомном Error

    // ПЛОХО — name останется 'Error', instanceof работает но имя неверно
    class NetworkError extends Error {
      constructor(message, status) {
        super(message)
        // name не установлен!
        this.status = status
      }
    }
    
    const err = new NetworkError('Not found', 404)
    console.log(err.name)   // 'Error' — неправильно
    console.log(err.stack)  // 'Error: Not found' — запутанный стек
    
    // ХОРОШО
    class NetworkError extends Error {
      constructor(message, status) {
        super(message)
        this.name = 'NetworkError'  // обязательно!
        this.status = status
      }
    }
    console.log(err.name)  // 'NetworkError'

    2. Вызвать super() с неправильными аргументами в extends Array

    // ПЛОХО — элементы не добавятся в массив
    class PowerArray extends Array {
      constructor(items) {
        super()          // пустой конструктор — items проигнорированы!
        this.push(items) // теперь в массиве один элемент — сам массив
      }
    }
    
    // ХОРОШО — spread передаёт элементы в конструктор Array
    class PowerArray extends Array {
      constructor(...items) {
        super(...items)  // корректно передаём элементы
      }
    }
    const arr = new PowerArray(1, 2, 3)
    console.log(arr.length)  // 3

    3. Ожидать, что Symbol.species работает в всех методах

    class MyArray extends Array {
      static get [Symbol.species]() { return Array }
    }
    
    const arr = new MyArray(1, 2, 3)
    const mapped = arr.map(x => x * 2)       // обычный Array (Symbol.species)
    const filtered = arr.filter(x => x > 1)  // обычный Array (Symbol.species)
    // НО:
    const copy = new MyArray(...arr)          // всё равно MyArray — конструктор явный

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

  • Библиотека Lodash: коллекция chain() возвращает обёртку над массивом с fluent API — аналог extends Array
  • TypeORM QueryBuilder: методы .where(), .orderBy(), .limit() — это extends на уровне паттерна Builder
  • Иерархия ошибок в Express.js: HttpError extends Error, NotFoundError extends HttpError — стандартный паттерн для всех Node.js серверов
  • Примеры

    PowerArray extends Array: методы sum(), average(), flatten(), groupBy() и демонстрация Symbol.species

    // PowerArray — расширенный массив с аналитическими методами
    class PowerArray extends Array {
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    
      average() {
        if (this.length === 0) return 0
        return this.sum() / this.length
      }
    
      min() { return Math.min(...this) }
      max() { return Math.max(...this) }
    
      flatten() {
        return this.reduce((acc, val) =>
          Array.isArray(val)
            ? acc.concat(new PowerArray(...val).flatten())
            : (acc.push(val), acc),
          new PowerArray()
        )
      }
    
      groupBy(keyFn) {
        return this.reduce((groups, item) => {
          const key = keyFn(item)
          if (!groups[key]) groups[key] = new PowerArray()
          groups[key].push(item)
          return groups
        }, {})
      }
    
      unique() {
        return new PowerArray(...new Set(this))
      }
    
      // chunk(2) → [[1,2],[3,4],[5]]
      chunk(size) {
        const chunks = []
        for (let i = 0; i < this.length; i += size) {
          chunks.push(new PowerArray(...this.slice(i, i + size)))
        }
        return chunks
      }
    }
    
    // Основные операции
    console.log('=== PowerArray ===')
    const prices = new PowerArray(1500, 3200, 800, 4700, 2100, 950)
    
    console.log('Сумма:', prices.sum())        // 13250
    console.log('Среднее:', prices.average())  // ~2208.3
    console.log('Мин:', prices.min())          // 800
    console.log('Макс:', prices.max())         // 4700
    
    // filter возвращает PowerArray — методы доступны в цепочке!
    const expensive = prices.filter(p => p > 2000)
    console.log('\nТовары дороже 2000:', [...expensive])  // [3200, 4700, 2100]
    console.log('instanceof PowerArray:', expensive instanceof PowerArray)  // true
    console.log('Сумма дорогих:', expensive.sum())  // 9800
    console.log('Средний дорогой:', expensive.average().toFixed(2))  // 3300.00
    
    // groupBy
    console.log('\n=== groupBy ===')
    const products = new PowerArray(
      { name: 'Молоко', category: 'молочные', price: 89 },
      { name: 'Кефир', category: 'молочные', price: 65 },
      { name: 'Хлеб', category: 'выпечка', price: 45 },
      { name: 'Батон', category: 'выпечка', price: 38 },
      { name: 'Сыр', category: 'молочные', price: 320 },
    )
    
    const byCategory = products.groupBy(p => p.category)
    for (const [cat, items] of Object.entries(byCategory)) {
      const prices2 = new PowerArray(...items.map(p => p.price))
      console.log(`${cat}: ${items.length} товара, сумма ${prices2.sum()} руб.`)
    }
    
    // chunk
    console.log('\n=== chunk (пагинация) ===')
    const ids = new PowerArray(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    const pages = ids.chunk(3)
    pages.forEach((page, i) => {
      console.log(`Страница ${i + 1}:`, [...page])
    })
    
    // flatten
    console.log('\n=== flatten ===')
    const nested = new PowerArray(
      new PowerArray(1, 2, 3),
      new PowerArray(4, 5),
      new PowerArray(6, 7, 8, 9),
    )
    const flat = nested.flatten()
    console.log('Исходный:', nested.map(a => [...a]))
    console.log('Плоский:', [...flat])     // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    console.log('Сумма:', flat.sum())      // 45
    
    // unique
    const withDups = new PowerArray(3, 1, 4, 1, 5, 9, 2, 6, 5, 3)
    console.log('\nС дубликатами:', [...withDups])
    console.log('Уникальные:', [...withDups.unique()])

    Расширение встроенных классов

    Представь: ты работаешь с данными аналитики — массивами чисел, которые нужно суммировать, группировать, разбивать на страницы. Каждый раз писать array.reduce(...) неудобно. Что если массив сам знал бы .sum(), .groupBy(), .chunk()? Это и есть расширение встроенных классов — добавление доменной логики прямо в структуру данных.

    Что решает этот механизм

    Встроенные классы (Array, Map, Set, Error) покрывают общие операции, но ничего не знают о твоей предметной области. Наследование позволяет добавить методы конкретного домена, сохранив всю мощь стандартного API.

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

  • классы, extends, super — механизм наследования, который используется здесь
  • Symbol — Symbol.species управляет типом возвращаемых значений методов
  • Map, Set — базовые классы, которые можно расширять
  • class PowerArray extends Array

    class PowerArray extends Array {
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    
      average() {
        if (this.length === 0) return 0
        return this.sum() / this.length
      }
    
      flatten() {
        return this.reduce((acc, val) =>
          acc.concat(Array.isArray(val) ? val : [val]), new PowerArray())
      }
    }
    
    const nums = new PowerArray(10, 20, 30, 40)
    console.log(nums.sum())      // 100
    console.log(nums.average())  // 25
    console.log(nums instanceof PowerArray)  // true
    console.log(nums instanceof Array)       // true

    Symbol.species — тип возвращаемых значений

    По умолчанию методы map, filter, slice возвращают тот же класс (PowerArray), а не обычный Array:

    const nums = new PowerArray(10, 20, 30, 40, 50)
    const filtered = nums.filter(n => n > 20)  // возвращает PowerArray!
    console.log(filtered instanceof PowerArray)  // true
    console.log(filtered.sum())                  // 120 — методы доступны

    Чтобы map/filter возвращали обычный Array, используй Symbol.species:

    class PowerArray extends Array {
      static get [Symbol.species]() {
        return Array  // map/filter/slice вернут обычный Array
      }
    
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    }
    
    const nums = new PowerArray(1, 2, 3)
    const doubled = nums.map(x => x * 2)   // обычный Array
    console.log(doubled instanceof PowerArray)  // false
    console.log(doubled instanceof Array)       // true
    // doubled.sum() — ошибка! sum() нет на Array

    Без Symbol.species цепочки трансформаций сохраняют расширенный тип — удобно для fluent API.

    Расширение Map

    class DefaultMap extends Map {
      constructor(defaultValue, entries) {
        super(entries)
        this._default = defaultValue
      }
    
      get(key) {
        if (!this.has(key)) {
          this.set(key, typeof this._default === 'function'
            ? this._default(key)
            : this._default)
        }
        return super.get(key)
      }
    }
    
    const counter = new DefaultMap(0)
    counter.set('apple', counter.get('apple') + 1)  // 1 (было 0 по умолчанию)
    counter.set('apple', counter.get('apple') + 1)  // 2
    counter.get('banana')  // 0 — создаётся запись по умолчанию

    Расширение Error

    Принято создавать иерархию ошибок для точной обработки через instanceof:

    class AppError extends Error {
      constructor(message, code) {
        super(message)
        this.name = 'AppError'  // ВАЖНО: установить name вручную
        this.code = code
      }
    }
    
    class NetworkError extends AppError {
      constructor(message, statusCode) {
        super(message, 'NETWORK_ERROR')
        this.name = 'NetworkError'
        this.statusCode = statusCode
      }
    }
    
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 'VALIDATION_ERROR')
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    // Обработка по типу
    try {
      throw new ValidationError('Поле обязательно', 'email')
    } catch (err) {
      if (err instanceof ValidationError) {
        console.log(`Ошибка в поле "${err.field}": ${err.message}`)
      } else if (err instanceof NetworkError) {
        console.log(`HTTP ${err.statusCode}: ${err.message}`)
      } else {
        throw err  // пробрасываем неизвестные ошибки
      }
    }

    Реальный пример: SortedArray

    class SortedArray extends Array {
      constructor(compareFn, ...items) {
        super()
        this._compare = compareFn || ((a, b) => a > b ? 1 : a < b ? -1 : 0)
        this.insertMany(items)
      }
    
      insert(item) {
        // Бинарный поиск позиции вставки
        let lo = 0, hi = this.length
        while (lo < hi) {
          const mid = (lo + hi) >> 1
          if (this._compare(this[mid], item) <= 0) lo = mid + 1
          else hi = mid
        }
        this.splice(lo, 0, item)
        return this
      }
    
      insertMany(items) {
        items.forEach(item => this.insert(item))
        return this
      }
    }

    Реальный пример: ObservableArray

    class ObservableArray extends Array {
      constructor(...args) {
        super(...args)
        this._listeners = []
      }
    
      onChange(handler) {
        this._listeners.push(handler)
        return () => {
          this._listeners = this._listeners.filter(h => h !== handler)
        }
      }
    
      _notify(action, items) {
        this._listeners.forEach(h => h({ action, items, length: this.length }))
      }
    
      push(...items) {
        const result = super.push(...items)
        this._notify('push', items)
        return result
      }
    
      pop() {
        const item = super.pop()
        this._notify('pop', [item])
        return item
      }
    }

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

    1. Забыть установить this.name в кастомном Error

    // ПЛОХО — name останется 'Error', instanceof работает но имя неверно
    class NetworkError extends Error {
      constructor(message, status) {
        super(message)
        // name не установлен!
        this.status = status
      }
    }
    
    const err = new NetworkError('Not found', 404)
    console.log(err.name)   // 'Error' — неправильно
    console.log(err.stack)  // 'Error: Not found' — запутанный стек
    
    // ХОРОШО
    class NetworkError extends Error {
      constructor(message, status) {
        super(message)
        this.name = 'NetworkError'  // обязательно!
        this.status = status
      }
    }
    console.log(err.name)  // 'NetworkError'

    2. Вызвать super() с неправильными аргументами в extends Array

    // ПЛОХО — элементы не добавятся в массив
    class PowerArray extends Array {
      constructor(items) {
        super()          // пустой конструктор — items проигнорированы!
        this.push(items) // теперь в массиве один элемент — сам массив
      }
    }
    
    // ХОРОШО — spread передаёт элементы в конструктор Array
    class PowerArray extends Array {
      constructor(...items) {
        super(...items)  // корректно передаём элементы
      }
    }
    const arr = new PowerArray(1, 2, 3)
    console.log(arr.length)  // 3

    3. Ожидать, что Symbol.species работает в всех методах

    class MyArray extends Array {
      static get [Symbol.species]() { return Array }
    }
    
    const arr = new MyArray(1, 2, 3)
    const mapped = arr.map(x => x * 2)       // обычный Array (Symbol.species)
    const filtered = arr.filter(x => x > 1)  // обычный Array (Symbol.species)
    // НО:
    const copy = new MyArray(...arr)          // всё равно MyArray — конструктор явный

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

  • Библиотека Lodash: коллекция chain() возвращает обёртку над массивом с fluent API — аналог extends Array
  • TypeORM QueryBuilder: методы .where(), .orderBy(), .limit() — это extends на уровне паттерна Builder
  • Иерархия ошибок в Express.js: HttpError extends Error, NotFoundError extends HttpError — стандартный паттерн для всех Node.js серверов
  • Примеры

    PowerArray extends Array: методы sum(), average(), flatten(), groupBy() и демонстрация Symbol.species

    // PowerArray — расширенный массив с аналитическими методами
    class PowerArray extends Array {
      sum() {
        return this.reduce((acc, val) => acc + val, 0)
      }
    
      average() {
        if (this.length === 0) return 0
        return this.sum() / this.length
      }
    
      min() { return Math.min(...this) }
      max() { return Math.max(...this) }
    
      flatten() {
        return this.reduce((acc, val) =>
          Array.isArray(val)
            ? acc.concat(new PowerArray(...val).flatten())
            : (acc.push(val), acc),
          new PowerArray()
        )
      }
    
      groupBy(keyFn) {
        return this.reduce((groups, item) => {
          const key = keyFn(item)
          if (!groups[key]) groups[key] = new PowerArray()
          groups[key].push(item)
          return groups
        }, {})
      }
    
      unique() {
        return new PowerArray(...new Set(this))
      }
    
      // chunk(2) → [[1,2],[3,4],[5]]
      chunk(size) {
        const chunks = []
        for (let i = 0; i < this.length; i += size) {
          chunks.push(new PowerArray(...this.slice(i, i + size)))
        }
        return chunks
      }
    }
    
    // Основные операции
    console.log('=== PowerArray ===')
    const prices = new PowerArray(1500, 3200, 800, 4700, 2100, 950)
    
    console.log('Сумма:', prices.sum())        // 13250
    console.log('Среднее:', prices.average())  // ~2208.3
    console.log('Мин:', prices.min())          // 800
    console.log('Макс:', prices.max())         // 4700
    
    // filter возвращает PowerArray — методы доступны в цепочке!
    const expensive = prices.filter(p => p > 2000)
    console.log('\nТовары дороже 2000:', [...expensive])  // [3200, 4700, 2100]
    console.log('instanceof PowerArray:', expensive instanceof PowerArray)  // true
    console.log('Сумма дорогих:', expensive.sum())  // 9800
    console.log('Средний дорогой:', expensive.average().toFixed(2))  // 3300.00
    
    // groupBy
    console.log('\n=== groupBy ===')
    const products = new PowerArray(
      { name: 'Молоко', category: 'молочные', price: 89 },
      { name: 'Кефир', category: 'молочные', price: 65 },
      { name: 'Хлеб', category: 'выпечка', price: 45 },
      { name: 'Батон', category: 'выпечка', price: 38 },
      { name: 'Сыр', category: 'молочные', price: 320 },
    )
    
    const byCategory = products.groupBy(p => p.category)
    for (const [cat, items] of Object.entries(byCategory)) {
      const prices2 = new PowerArray(...items.map(p => p.price))
      console.log(`${cat}: ${items.length} товара, сумма ${prices2.sum()} руб.`)
    }
    
    // chunk
    console.log('\n=== chunk (пагинация) ===')
    const ids = new PowerArray(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    const pages = ids.chunk(3)
    pages.forEach((page, i) => {
      console.log(`Страница ${i + 1}:`, [...page])
    })
    
    // flatten
    console.log('\n=== flatten ===')
    const nested = new PowerArray(
      new PowerArray(1, 2, 3),
      new PowerArray(4, 5),
      new PowerArray(6, 7, 8, 9),
    )
    const flat = nested.flatten()
    console.log('Исходный:', nested.map(a => [...a]))
    console.log('Плоский:', [...flat])     // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    console.log('Сумма:', flat.sum())      // 45
    
    // unique
    const withDups = new PowerArray(3, 1, 4, 1, 5, 9, 2, 6, 5, 3)
    console.log('\nС дубликатами:', [...withDups])
    console.log('Уникальные:', [...withDups.unique()])

    Задание

    Создай класс TypedMap, наследующийся от Map, который хранит пары строка→число. Добавь методы: sum() — сумма всех значений, average() — среднее значение, filter(predicate) — возвращает новый TypedMap с парами, для которых predicate(key, value) истинен, top(n) — возвращает n пар с наибольшими значениями в виде массива [key, value].

    Подсказка

    sum: for (const value of this.values()). average: return this.sum() / this.size. filter: for (const [key, value] of this.entries()). top: sort((a, b) => b[1] - a[1]).slice(0, n)

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