В системе интернет-магазина есть User, Product, Order. Каждому нужна сериализация в JSON, временные метки и система событий. Наследование не подходит — нельзя наследовать от трёх классов одновременно. Решение — примеси: наборы методов, которые копируются в любой класс.
JavaScript поддерживает только одиночное наследование — класс может наследовать только от одного другого класса. Но что делать, если нужно добавить к классу несколько независимых наборов поведения?
class User extends Serializable, Validatable { } // ОШИБКА — так нельзя!Примесь — это обычный объект с методами, которые можно скопировать в прототип любого класса через Object.assign:
const Serializable = {
toJSON() {
return JSON.stringify(this)
},
fromJSON(json) {
return Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(json))
},
}
class User {
constructor(name, email) {
this.name = name
this.email = email
}
}
// Копируем методы примеси в прототип класса
Object.assign(User.prototype, Serializable)
const user = new User('Иван', 'ivan@example.ru')
console.log(user.toJSON()) // '{"name":"Иван","email":"ivan@example.ru"}'| | Наследование | Mixin | Интерфейс (TypeScript) |
|---|---|---|---|
| Реализация | Да | Да | Нет |
| Множественное | Нет | Да | Да |
| Связь классов | Жёсткая | Слабая | Контракт |
| JS-поддержка | Да | Да | Только TS |
const Serializable = {
serialize() {
return JSON.stringify(this)
},
toObject() {
return JSON.parse(JSON.stringify(this))
},
}const Timestamped = {
setTimestamps() {
this.createdAt = this.createdAt || new Date().toISOString()
this.updatedAt = new Date().toISOString()
},
}const EventEmitter = {
on(event, listener) {
if (!this._listeners) this._listeners = {}
if (!this._listeners[event]) this._listeners[event] = []
this._listeners[event].push(listener)
return this
},
emit(event, ...args) {
if (!this._listeners || !this._listeners[event]) return
this._listeners[event].forEach(fn => fn(...args))
},
off(event, listener) {
if (!this._listeners || !this._listeners[event]) return
this._listeners[event] = this._listeners[event].filter(fn => fn !== listener)
},
}class Product {
constructor(name, price) {
this.name = name
this.price = price
}
}
// Применяем сразу несколько примесей
Object.assign(Product.prototype, Serializable, Timestamped, EventEmitter)
const p = new Product('Ноутбук', 89990)
p.setTimestamps()
console.log(p.serialize()) // {"name":"Ноутбук","price":89990,"createdAt":"..."}
p.on('priceChanged', (newPrice) => console.log('Цена изменена:', newPrice))
p.emit('priceChanged', 79990) // 'Цена изменена: 79990'1. Mixin не должен иметь конструктора — только методы
2. Методы примеси используют this — они работают в контексте объекта
3. Следите за коллизиями имён — Object.assign перезапишет метод класса
4. Примеси для несвязанных capabilities: сериализация, валидация, события
Ошибка 1: коллизия имён — примесь перезаписывает метод класса
const Logging = {
toString() { return '[Logging]' } // перезапишет toString класса!
}
class Product {
toString() { return `Product: ${this.name}` }
}
Object.assign(Product.prototype, Logging)
const p = new Product()
p.toString() // '[Logging]' — метод класса перезаписан!
// Исправлено: проверяем перед применением
function applyMixin(TargetClass, mixin) {
for (const key of Object.keys(mixin)) {
if (key in TargetClass.prototype) {
console.warn(`Коллизия: метод "${key}" в ${TargetClass.name} будет перезаписан`)
}
}
Object.assign(TargetClass.prototype, mixin)
}Ошибка 2: примесь с конструктором
// Сломано: мixin не должен иметь конструктора
const BadMixin = {
constructor() { // проблема — Object.assign перенесёт это!
this.created = Date.now()
},
getCreated() { return this.created }
}
// Исправлено: инициализацию вызывают явно как метод
const GoodMixin = {
initTimestamps() { this.createdAt = Date.now() },
getCreated() { return this.createdAt }
}Ошибка 3: примесь для поведения, которое лучше решается наследованием
// Сломано: AdminUser и User — явная иерархия, mixin лишний
const AdminMixin = { canDelete: () => true, canBan: () => true }
Object.assign(User.prototype, AdminMixin) // все Users стали Admin!
// Исправлено: наследование для "is-a" отношений
class AdminUser extends User {
canDelete() { return true }
canBan() { return true }
}(Base: T) => class extends Base { ... } — типобезопасноПримеси Serializable и Timestamped применяются к классу User через Object.assign
// Примесь 1: сериализация
const Serializable = {
serialize() {
return JSON.stringify(this)
},
toObject() {
return JSON.parse(JSON.stringify(this))
},
clone() {
const obj = Object.create(Object.getPrototypeOf(this))
return Object.assign(obj, JSON.parse(JSON.stringify(this)))
},
}
// Примесь 2: временные метки
const Timestamped = {
touch() {
if (!this.createdAt) {
this.createdAt = new Date().toISOString()
}
this.updatedAt = new Date().toISOString()
return this
},
getAge() {
if (!this.createdAt) return null
const ms = Date.now() - new Date(this.createdAt).getTime()
return Math.floor(ms / 1000) // секунды
},
}
// Классы — без каких-либо знаний о сериализации и временных метках
class User {
constructor(name, email, role) {
this.name = name
this.email = email
this.role = role
}
greet() {
return `Привет, я ${this.name}!`
}
}
class Product {
constructor(name, price, category) {
this.name = name
this.price = price
this.category = category
}
getPriceFormatted() {
return this.price.toLocaleString('ru-RU') + ' ₽'
}
}
// Применяем примеси к обоим классам
Object.assign(User.prototype, Serializable, Timestamped)
Object.assign(Product.prototype, Serializable, Timestamped)
// User
const user = new User('Мария Иванова', 'maria@example.ru', 'admin')
user.touch()
console.log('User:')
console.log(user.greet()) // 'Привет, я Мария Иванова!'
console.log(user.serialize()) // JSON-строка
console.log(user.toObject()) // { name, email, role, createdAt, updatedAt }
console.log('Возраст (сек):', user.getAge()) // ~0
const userClone = user.clone()
userClone.name = 'Клон Марии'
console.log('\nОригинал:', user.name) // 'Мария Иванова'
console.log('Клон:', userClone.name) // 'Клон Марии'
// Product
const product = new Product('iPhone 15', 89990, 'Смартфоны')
product.touch()
console.log('\nProduct:')
console.log(product.getPriceFormatted()) // '89 990 ₽'
console.log(product.serialize()) // JSON-строка
// Оба класса получили одни и те же возможности без наследования!
console.log('\nUser имеет serialize:', typeof user.serialize) // function
console.log('Product имеет serialize:', typeof product.serialize) // functionВ системе интернет-магазина есть User, Product, Order. Каждому нужна сериализация в JSON, временные метки и система событий. Наследование не подходит — нельзя наследовать от трёх классов одновременно. Решение — примеси: наборы методов, которые копируются в любой класс.
JavaScript поддерживает только одиночное наследование — класс может наследовать только от одного другого класса. Но что делать, если нужно добавить к классу несколько независимых наборов поведения?
class User extends Serializable, Validatable { } // ОШИБКА — так нельзя!Примесь — это обычный объект с методами, которые можно скопировать в прототип любого класса через Object.assign:
const Serializable = {
toJSON() {
return JSON.stringify(this)
},
fromJSON(json) {
return Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(json))
},
}
class User {
constructor(name, email) {
this.name = name
this.email = email
}
}
// Копируем методы примеси в прототип класса
Object.assign(User.prototype, Serializable)
const user = new User('Иван', 'ivan@example.ru')
console.log(user.toJSON()) // '{"name":"Иван","email":"ivan@example.ru"}'| | Наследование | Mixin | Интерфейс (TypeScript) |
|---|---|---|---|
| Реализация | Да | Да | Нет |
| Множественное | Нет | Да | Да |
| Связь классов | Жёсткая | Слабая | Контракт |
| JS-поддержка | Да | Да | Только TS |
const Serializable = {
serialize() {
return JSON.stringify(this)
},
toObject() {
return JSON.parse(JSON.stringify(this))
},
}const Timestamped = {
setTimestamps() {
this.createdAt = this.createdAt || new Date().toISOString()
this.updatedAt = new Date().toISOString()
},
}const EventEmitter = {
on(event, listener) {
if (!this._listeners) this._listeners = {}
if (!this._listeners[event]) this._listeners[event] = []
this._listeners[event].push(listener)
return this
},
emit(event, ...args) {
if (!this._listeners || !this._listeners[event]) return
this._listeners[event].forEach(fn => fn(...args))
},
off(event, listener) {
if (!this._listeners || !this._listeners[event]) return
this._listeners[event] = this._listeners[event].filter(fn => fn !== listener)
},
}class Product {
constructor(name, price) {
this.name = name
this.price = price
}
}
// Применяем сразу несколько примесей
Object.assign(Product.prototype, Serializable, Timestamped, EventEmitter)
const p = new Product('Ноутбук', 89990)
p.setTimestamps()
console.log(p.serialize()) // {"name":"Ноутбук","price":89990,"createdAt":"..."}
p.on('priceChanged', (newPrice) => console.log('Цена изменена:', newPrice))
p.emit('priceChanged', 79990) // 'Цена изменена: 79990'1. Mixin не должен иметь конструктора — только методы
2. Методы примеси используют this — они работают в контексте объекта
3. Следите за коллизиями имён — Object.assign перезапишет метод класса
4. Примеси для несвязанных capabilities: сериализация, валидация, события
Ошибка 1: коллизия имён — примесь перезаписывает метод класса
const Logging = {
toString() { return '[Logging]' } // перезапишет toString класса!
}
class Product {
toString() { return `Product: ${this.name}` }
}
Object.assign(Product.prototype, Logging)
const p = new Product()
p.toString() // '[Logging]' — метод класса перезаписан!
// Исправлено: проверяем перед применением
function applyMixin(TargetClass, mixin) {
for (const key of Object.keys(mixin)) {
if (key in TargetClass.prototype) {
console.warn(`Коллизия: метод "${key}" в ${TargetClass.name} будет перезаписан`)
}
}
Object.assign(TargetClass.prototype, mixin)
}Ошибка 2: примесь с конструктором
// Сломано: мixin не должен иметь конструктора
const BadMixin = {
constructor() { // проблема — Object.assign перенесёт это!
this.created = Date.now()
},
getCreated() { return this.created }
}
// Исправлено: инициализацию вызывают явно как метод
const GoodMixin = {
initTimestamps() { this.createdAt = Date.now() },
getCreated() { return this.createdAt }
}Ошибка 3: примесь для поведения, которое лучше решается наследованием
// Сломано: AdminUser и User — явная иерархия, mixin лишний
const AdminMixin = { canDelete: () => true, canBan: () => true }
Object.assign(User.prototype, AdminMixin) // все Users стали Admin!
// Исправлено: наследование для "is-a" отношений
class AdminUser extends User {
canDelete() { return true }
canBan() { return true }
}(Base: T) => class extends Base { ... } — типобезопасноПримеси Serializable и Timestamped применяются к классу User через Object.assign
// Примесь 1: сериализация
const Serializable = {
serialize() {
return JSON.stringify(this)
},
toObject() {
return JSON.parse(JSON.stringify(this))
},
clone() {
const obj = Object.create(Object.getPrototypeOf(this))
return Object.assign(obj, JSON.parse(JSON.stringify(this)))
},
}
// Примесь 2: временные метки
const Timestamped = {
touch() {
if (!this.createdAt) {
this.createdAt = new Date().toISOString()
}
this.updatedAt = new Date().toISOString()
return this
},
getAge() {
if (!this.createdAt) return null
const ms = Date.now() - new Date(this.createdAt).getTime()
return Math.floor(ms / 1000) // секунды
},
}
// Классы — без каких-либо знаний о сериализации и временных метках
class User {
constructor(name, email, role) {
this.name = name
this.email = email
this.role = role
}
greet() {
return `Привет, я ${this.name}!`
}
}
class Product {
constructor(name, price, category) {
this.name = name
this.price = price
this.category = category
}
getPriceFormatted() {
return this.price.toLocaleString('ru-RU') + ' ₽'
}
}
// Применяем примеси к обоим классам
Object.assign(User.prototype, Serializable, Timestamped)
Object.assign(Product.prototype, Serializable, Timestamped)
// User
const user = new User('Мария Иванова', 'maria@example.ru', 'admin')
user.touch()
console.log('User:')
console.log(user.greet()) // 'Привет, я Мария Иванова!'
console.log(user.serialize()) // JSON-строка
console.log(user.toObject()) // { name, email, role, createdAt, updatedAt }
console.log('Возраст (сек):', user.getAge()) // ~0
const userClone = user.clone()
userClone.name = 'Клон Марии'
console.log('\nОригинал:', user.name) // 'Мария Иванова'
console.log('Клон:', userClone.name) // 'Клон Марии'
// Product
const product = new Product('iPhone 15', 89990, 'Смартфоны')
product.touch()
console.log('\nProduct:')
console.log(product.getPriceFormatted()) // '89 990 ₽'
console.log(product.serialize()) // JSON-строка
// Оба класса получили одни и те же возможности без наследования!
console.log('\nUser имеет serialize:', typeof user.serialize) // function
console.log('Product имеет serialize:', typeof product.serialize) // functionСоздай примесь Validatable с методом validate(), который проверяет, что все поля из массива this.requiredFields заполнены (не null и не undefined и не пустая строка). Примени эту примесь к классам User и Product. Оба класса должны определять свой массив requiredFields.
const Validatable = { validate() { return this.requiredFields.every(f => this[f] != null && this[f] !== '') } }; Object.assign(User.prototype, Validatable); Object.assign(Product.prototype, Validatable)