В Stripe объект PaymentIntent имеет свойство amount — сумма в центах. Но разработчики API решили: при записи суммы нужно валидировать что она больше нуля, а при чтении — автоматически форматировать в рубли. Это и делают геттеры и сеттеры: невидимая логика за простым синтаксисом свойства.
Прямое присвоение obj.value = -100 не позволяет добавить проверку или побочный эффект. Геттеры и сеттеры позволяют выполнять код при чтении или записи свойства, оставляя синтаксис «как у обычного свойства» (obj.value без скобок).
const cart = {
_items: [],
_discount: 0,
get total() {
const subtotal = this._items.reduce((sum, item) => sum + item.price, 0)
return subtotal * (1 - this._discount)
},
set discount(value) {
if (value < 0 || value > 1) throw new RangeError('Скидка от 0 до 1')
this._discount = value
},
get discount() {
return this._discount
}
}
cart._items = [{ price: 1000 }, { price: 500 }]
cart.discount = 0.1 // вызывает setter
console.log(cart.total) // 1350 — вычисляется автоматически
cart.discount = 1.5 // RangeError!class Product {
constructor(name, priceKopecks) {
this._name = name
this._price = priceKopecks // хранится в копейках
}
get price() {
return this._price / 100 // геттер отдаёт в рублях
}
set price(rubles) {
if (rubles < 0) throw new RangeError('Цена не может быть отрицательной')
this._price = Math.round(rubles * 100) // сеттер конвертирует в копейки
}
get displayName() {
return `${this._name} — ${this.price} ₽`
}
}
const p = new Product('Ноутбук', 5000000) // 50000 ₽ в копейках
console.log(p.price) // 50000
console.log(p.displayName) // 'Ноутбук — 50000 ₽'
p.price = 45000 // конвертирует в копейки при записи
console.log(p._price) // 4500000class Circle {
constructor(radius) {
this.radius = radius // через сеттер — с валидацией
}
get radius() { return this._radius }
set radius(value) {
if (value < 0) throw new RangeError('Радиус не может быть отрицательным')
this._radius = value
}
get area() {
return Math.PI * this._radius ** 2 // read-only вычисляемое свойство
}
get circumference() {
return 2 * Math.PI * this._radius // read-only
}
}Для существующих объектов геттеры/сеттеры добавляются через Object.defineProperty:
const config = { _maxRetries: 3 }
Object.defineProperty(config, 'maxRetries', {
get() { return this._maxRetries },
set(v) {
if (!Number.isInteger(v) || v < 1) throw new Error('Должно быть целое число >= 1')
this._maxRetries = v
},
enumerable: true,
configurable: true,
})
config.maxRetries = 5
console.log(config.maxRetries) // 5
config.maxRetries = 0 // Error| Геттер | Метод |
|--------|-------|
| user.fullName — без скобок | user.getFullName() — со скобками |
| Семантически «свойство» | Семантически «действие» |
| Для вычисляемых значений | Для действий с побочными эффектами |
Ошибка 1: не используешь сеттер в конструкторе — пропускаешь валидацию
class Temperature {
constructor(celsius) {
this._celsius = celsius // Прямое присвоение — обходит сеттер!
}
set celsius(value) {
if (value < -273.15) throw new RangeError('Ниже абсолютного нуля')
this._celsius = value
}
}
const t = new Temperature(-300) // Не выбросит ошибку!
// Правильно: в конструкторе используй сеттер
constructor(celsius) {
this.celsius = celsius // без _ — через сеттер, с валидацией
}Ошибка 2: бесконечная рекурсия в геттере/сеттере
class User {
get name() {
return this.name // ОШИБКА: геттер вызывает сам себя бесконечно!
}
// Правильно: приватное поле через _
get name() {
return this._name // читаем из _name, не из name
}
}Ошибка 3: геттер с побочным эффектом — неожиданное поведение
class Logger {
get lastEntry() {
this._entries.push('read') // побочный эффект в геттере — плохая практика!
return this._entries[this._entries.length - 1]
}
}
// Геттер должен только читать, не изменять состояниеКласс корзины интернет-магазина с геттерами для вычисления суммы и сеттером валидации
class ShoppingCart {
constructor(currency = 'RUB') {
this._items = []
this._discount = 0
this._currency = currency
}
// Сеттер: валидация скидки при записи
get discount() { return this._discount }
set discount(value) {
if (typeof value !== 'number' || value < 0 || value > 1) {
throw new RangeError('Скидка должна быть числом от 0 до 1')
}
this._discount = value
}
// Геттеры: вычисляемые свойства
get subtotal() {
return this._items.reduce((sum, item) => sum + item.price * item.qty, 0)
}
get total() {
return this.subtotal * (1 - this._discount)
}
get itemCount() {
return this._items.reduce((sum, item) => sum + item.qty, 0)
}
get isEmpty() {
return this._items.length === 0
}
get summary() {
return `${this.itemCount} товара, ${this.total} ${this._currency}`
}
addItem(name, price, qty = 1) {
const existing = this._items.find(i => i.name === name)
if (existing) existing.qty += qty
else this._items.push({ name, price, qty })
}
}
const cart = new ShoppingCart()
console.log(cart.isEmpty) // true
cart.addItem('Ноутбук', 50000)
cart.addItem('Мышь', 1200, 2)
console.log(cart.subtotal) // 52400
console.log(cart.itemCount) // 3
cart.discount = 0.1
console.log(cart.total) // 47160
console.log(cart.summary) // '3 товара, 47160 RUB'
try {
cart.discount = 1.5 // > 1 — ошибка
} catch (e) {
console.log(e.message) // 'Скидка должна быть числом от 0 до 1'
}В Stripe объект PaymentIntent имеет свойство amount — сумма в центах. Но разработчики API решили: при записи суммы нужно валидировать что она больше нуля, а при чтении — автоматически форматировать в рубли. Это и делают геттеры и сеттеры: невидимая логика за простым синтаксисом свойства.
Прямое присвоение obj.value = -100 не позволяет добавить проверку или побочный эффект. Геттеры и сеттеры позволяют выполнять код при чтении или записи свойства, оставляя синтаксис «как у обычного свойства» (obj.value без скобок).
const cart = {
_items: [],
_discount: 0,
get total() {
const subtotal = this._items.reduce((sum, item) => sum + item.price, 0)
return subtotal * (1 - this._discount)
},
set discount(value) {
if (value < 0 || value > 1) throw new RangeError('Скидка от 0 до 1')
this._discount = value
},
get discount() {
return this._discount
}
}
cart._items = [{ price: 1000 }, { price: 500 }]
cart.discount = 0.1 // вызывает setter
console.log(cart.total) // 1350 — вычисляется автоматически
cart.discount = 1.5 // RangeError!class Product {
constructor(name, priceKopecks) {
this._name = name
this._price = priceKopecks // хранится в копейках
}
get price() {
return this._price / 100 // геттер отдаёт в рублях
}
set price(rubles) {
if (rubles < 0) throw new RangeError('Цена не может быть отрицательной')
this._price = Math.round(rubles * 100) // сеттер конвертирует в копейки
}
get displayName() {
return `${this._name} — ${this.price} ₽`
}
}
const p = new Product('Ноутбук', 5000000) // 50000 ₽ в копейках
console.log(p.price) // 50000
console.log(p.displayName) // 'Ноутбук — 50000 ₽'
p.price = 45000 // конвертирует в копейки при записи
console.log(p._price) // 4500000class Circle {
constructor(radius) {
this.radius = radius // через сеттер — с валидацией
}
get radius() { return this._radius }
set radius(value) {
if (value < 0) throw new RangeError('Радиус не может быть отрицательным')
this._radius = value
}
get area() {
return Math.PI * this._radius ** 2 // read-only вычисляемое свойство
}
get circumference() {
return 2 * Math.PI * this._radius // read-only
}
}Для существующих объектов геттеры/сеттеры добавляются через Object.defineProperty:
const config = { _maxRetries: 3 }
Object.defineProperty(config, 'maxRetries', {
get() { return this._maxRetries },
set(v) {
if (!Number.isInteger(v) || v < 1) throw new Error('Должно быть целое число >= 1')
this._maxRetries = v
},
enumerable: true,
configurable: true,
})
config.maxRetries = 5
console.log(config.maxRetries) // 5
config.maxRetries = 0 // Error| Геттер | Метод |
|--------|-------|
| user.fullName — без скобок | user.getFullName() — со скобками |
| Семантически «свойство» | Семантически «действие» |
| Для вычисляемых значений | Для действий с побочными эффектами |
Ошибка 1: не используешь сеттер в конструкторе — пропускаешь валидацию
class Temperature {
constructor(celsius) {
this._celsius = celsius // Прямое присвоение — обходит сеттер!
}
set celsius(value) {
if (value < -273.15) throw new RangeError('Ниже абсолютного нуля')
this._celsius = value
}
}
const t = new Temperature(-300) // Не выбросит ошибку!
// Правильно: в конструкторе используй сеттер
constructor(celsius) {
this.celsius = celsius // без _ — через сеттер, с валидацией
}Ошибка 2: бесконечная рекурсия в геттере/сеттере
class User {
get name() {
return this.name // ОШИБКА: геттер вызывает сам себя бесконечно!
}
// Правильно: приватное поле через _
get name() {
return this._name // читаем из _name, не из name
}
}Ошибка 3: геттер с побочным эффектом — неожиданное поведение
class Logger {
get lastEntry() {
this._entries.push('read') // побочный эффект в геттере — плохая практика!
return this._entries[this._entries.length - 1]
}
}
// Геттер должен только читать, не изменять состояниеКласс корзины интернет-магазина с геттерами для вычисления суммы и сеттером валидации
class ShoppingCart {
constructor(currency = 'RUB') {
this._items = []
this._discount = 0
this._currency = currency
}
// Сеттер: валидация скидки при записи
get discount() { return this._discount }
set discount(value) {
if (typeof value !== 'number' || value < 0 || value > 1) {
throw new RangeError('Скидка должна быть числом от 0 до 1')
}
this._discount = value
}
// Геттеры: вычисляемые свойства
get subtotal() {
return this._items.reduce((sum, item) => sum + item.price * item.qty, 0)
}
get total() {
return this.subtotal * (1 - this._discount)
}
get itemCount() {
return this._items.reduce((sum, item) => sum + item.qty, 0)
}
get isEmpty() {
return this._items.length === 0
}
get summary() {
return `${this.itemCount} товара, ${this.total} ${this._currency}`
}
addItem(name, price, qty = 1) {
const existing = this._items.find(i => i.name === name)
if (existing) existing.qty += qty
else this._items.push({ name, price, qty })
}
}
const cart = new ShoppingCart()
console.log(cart.isEmpty) // true
cart.addItem('Ноутбук', 50000)
cart.addItem('Мышь', 1200, 2)
console.log(cart.subtotal) // 52400
console.log(cart.itemCount) // 3
cart.discount = 0.1
console.log(cart.total) // 47160
console.log(cart.summary) // '3 товара, 47160 RUB'
try {
cart.discount = 1.5 // > 1 — ошибка
} catch (e) {
console.log(e.message) // 'Скидка должна быть числом от 0 до 1'
}Создай класс Temperature для конвертации температур. Храни значение в градусах Цельсия. Добавь геттер и сеттер celsius (с валидацией >= -273.15), геттеры fahrenheit (C * 9/5 + 32) и kelvin (C + 273.15), геттер description (строковое описание вроде "Кипение воды").
В конструкторе пиши this.celsius = celsius (без _) чтобы использовать сеттер с валидацией. Геттер fahrenheit: return this._celsius * 9/5 + 32. Геттер description: if/else if по значению this._celsius.