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

Геттеры и сеттеры

В Stripe объект PaymentIntent имеет свойство amount — сумма в центах. Но разработчики API решили: при записи суммы нужно валидировать что она больше нуля, а при чтении — автоматически форматировать в рубли. Это и делают геттеры и сеттеры: невидимая логика за простым синтаксисом свойства.

Какую проблему решает

Прямое присвоение obj.value = -100 не позволяет добавить проверку или побочный эффект. Геттеры и сеттеры позволяют выполнять код при чтении или записи свойства, оставляя синтаксис «как у обычного свойства» (obj.value без скобок).

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

  • Объекты — свойства объектов
  • Классы — class, constructor
  • try/catch — выброс ошибок через throw
  • Синтаксис в литерале объекта

    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)       // 4500000

    Геттер без сеттера — read-only свойство

    class 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

    Для существующих объектов геттеры/сеттеры добавляются через 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

    Геттер vs метод

    | Геттер | Метод |

    |--------|-------|

    | 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]
      }
    }
    // Геттер должен только читать, не изменять состояние

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

  • ORM (Sequelize, Prisma): виртуальные поля вычисляются из других полей
  • Vue.js computed: реактивные вычисляемые свойства — это геттеры
  • MobX: наблюдаемые свойства через геттеры/сеттеры
  • Валидация форм: сеттер проверяет данные до сохранения
  • Примеры

    Класс корзины интернет-магазина с геттерами для вычисления суммы и сеттером валидации

    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 без скобок).

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

  • Объекты — свойства объектов
  • Классы — class, constructor
  • try/catch — выброс ошибок через throw
  • Синтаксис в литерале объекта

    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)       // 4500000

    Геттер без сеттера — read-only свойство

    class 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

    Для существующих объектов геттеры/сеттеры добавляются через 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

    Геттер vs метод

    | Геттер | Метод |

    |--------|-------|

    | 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]
      }
    }
    // Геттер должен только читать, не изменять состояние

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

  • ORM (Sequelize, Prisma): виртуальные поля вычисляются из других полей
  • Vue.js computed: реактивные вычисляемые свойства — это геттеры
  • MobX: наблюдаемые свойства через геттеры/сеттеры
  • Валидация форм: сеттер проверяет данные до сохранения
  • Примеры

    Класс корзины интернет-магазина с геттерами для вычисления суммы и сеттером валидации

    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.

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