Представь: ты работаешь с данными аналитики — массивами чисел, которые нужно суммировать, группировать, разбивать на страницы. Каждый раз писать array.reduce(...) неудобно. Что если массив сам знал бы .sum(), .groupBy(), .chunk()? Это и есть расширение встроенных классов — добавление доменной логики прямо в структуру данных.
Встроенные классы (Array, Map, Set, Error) покрывают общие операции, но ничего не знают о твоей предметной области. Наследование позволяет добавить методы конкретного домена, сохранив всю мощь стандартного API.
Symbol.species управляет типом возвращаемых значений методов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По умолчанию методы 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.
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 — создаётся запись по умолчаниюПринято создавать иерархию ошибок для точной обработки через 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 // пробрасываем неизвестные ошибки
}
}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
}
}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) // 33. Ожидать, что 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 — конструктор явныйchain() возвращает обёртку над массивом с fluent API — аналог extends Array.where(), .orderBy(), .limit() — это extends на уровне паттерна BuilderHttpError 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.
Symbol.species управляет типом возвращаемых значений методов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По умолчанию методы 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.
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 — создаётся запись по умолчаниюПринято создавать иерархию ошибок для точной обработки через 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 // пробрасываем неизвестные ошибки
}
}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
}
}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) // 33. Ожидать, что 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 — конструктор явныйchain() возвращает обёртку над массивом с fluent API — аналог extends Array.where(), .orderBy(), .limit() — это extends на уровне паттерна BuilderHttpError 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)