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

Приватные и защищённые поля классов

В финансовом приложении класс BankAccount хранит баланс. Если разработчик случайно сделает account.balance = 99999 — деньги появятся из ниоткуда. Приватные поля #balance делают это физически невозможным: попытка доступа снаружи класса — синтаксическая ошибка ещё до запуска кода.

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

  • «Классы» — синтаксис class, constructor, методы
  • «Наследование» — extends, super; приватные поля не наследуются
  • «Геттеры/сеттеры» — get/set для управляемого доступа к полям
  • Соглашение _protected (конвенция)

    Поле с префиксом _ — это соглашение между разработчиками: "не трогай снаружи". Технически оно доступно откуда угодно:

    class Database {
      constructor(url) {
        this._connectionString = url  // "защищено" по соглашению
        this._isConnected = false
      }
    
      connect() {
        this._isConnected = true
        console.log('Подключено к', this._connectionString)
      }
    }
    
    const db = new Database('postgres://localhost:5432/mydb')
    // Технически можно обратиться — защита только на уровне договорённости:
    console.log(db._connectionString)  // 'postgres://localhost:5432/mydb'

    Настоящие приватные поля # (ES2022)

    Поля с # — это настоящая приватность на уровне языка. Обратиться к ним снаружи класса невозможно — будет ошибка:

    class BankAccount {
      #balance = 0
      #transactions = []
    
      constructor(initialBalance) {
        this.#balance = initialBalance
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма должна быть положительной')
        this.#balance += amount
        this.#transactions.push({ type: 'deposit', amount, date: new Date() })
      }
    
      withdraw(amount) {
        if (amount > this.#balance) throw new Error('Недостаточно средств')
        this.#balance -= amount
        this.#transactions.push({ type: 'withdraw', amount, date: new Date() })
      }
    
      get balance() {
        return this.#balance  // геттер — readonly доступ снаружи
      }
    
      get history() {
        return [...this.#transactions]  // копия — снаружи нельзя изменить оригинал
      }
    }
    
    const acc = new BankAccount(5000)
    acc.deposit(2000)
    acc.withdraw(500)
    console.log(acc.balance)     // 6500 — через геттер
    // acc.#balance              // SyntaxError — нет доступа!

    Приватные методы #method()

    class PaymentProcessor {
      #apiKey
    
      constructor(apiKey) {
        this.#apiKey = apiKey
      }
    
      #validateAmount(amount) {  // приватный метод — только для внутреннего использования
        if (amount <= 0) throw new Error('Некорректная сумма')
        if (amount > 1_000_000) throw new Error('Превышен лимит')
      }
    
      charge(amount) {
        this.#validateAmount(amount)  // публичный метод вызывает приватный
        console.log(`Списание ${amount} руб. с ключом ${this.#apiKey.slice(0, 4)}...`)
      }
    }

    Статические приватные поля

    class IdGenerator {
      static #count = 0  // общий счётчик для всех экземпляров
    
      static next() {
        return ++IdGenerator.#count
      }
    
      static get total() {
        return IdGenerator.#count
      }
    }
    
    IdGenerator.next()  // 1
    IdGenerator.next()  // 2
    console.log(IdGenerator.total)  // 2
    // IdGenerator.#count  // SyntaxError

    Сравнение # и _ подходов

    | Критерий | _ (конвенция) | # (ES2022) |

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

    | Реальная защита | Нет | Да |

    | Доступ в подклассах | Да | Нет (нужен геттер/метод) |

    | Поддержка браузеров | Все | Современные (2021+) |

    | TypeScript | private / protected | Полная поддержка |

    Геттер для readonly доступа

    Приватное поле + геттер без сеттера = читаемое, но не изменяемое извне свойство:

    class Order {
      #id
      #status = 'pending'
    
      constructor() {
        this.#id = Math.random().toString(36).slice(2).toUpperCase()
      }
    
      get id() { return this.#id }
      get status() { return this.#status }
    
      confirm() { this.#status = 'confirmed' }
      cancel()  { this.#status = 'cancelled' }
      // Сеттера нет — статус меняется только через методы
    }

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

    1. Доступ к приватному полю из подкласса:

    class Animal {
      #name
    
      constructor(name) { this.#name = name }
      get name() { return this.#name }
    }
    
    class Dog extends Animal {
      bark() {
        // return this.#name  // SyntaxError! # поля не наследуются
        return this.name     // Правильно: через геттер родителя
      }
    }

    2. Попытка сериализации объекта с приватными полями через JSON.stringify:

    class User {
      #password
      name
    
      constructor(name, password) {
        this.name = name
        this.#password = password
      }
    }
    
    const user = new User('Иван', 'secret123')
    console.log(JSON.stringify(user))  // '{"name":"Иван"}' — #password не включается, это хорошо!
    // Но если нужно больше полей в JSON — добавь toJSON():
    // toJSON() { return { name: this.name, id: this.#id } }

    3. Использование # в коде задания с target ES2017 — ошибка компиляции в старых средах:

    // В учебных средах с ограниченным ES target
    // # поля могут не поддерживаться — используй _prefix конвенцию
    // В продакшн с современным target — # всегда предпочтительнее

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

  • Финансовые сервисы — баланс, история транзакций, PIN-коды
  • Аутентификация — хэш пароля, токены сессий
  • Кэши и пулы соединений — внутренний state не должен быть доступен потребителю
  • Счётчики и генераторы ID — статические приватные поля
  • Примеры

    Класс BankAccount с приватными полями #balance и #transactions, геттерами и цепочкой вызовов

    class BankAccount {
      #balance
      #transactions
      #owner
    
      constructor(owner, initialBalance = 0) {
        this.#owner = owner
        this.#balance = initialBalance
        this.#transactions = []
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма пополнения должна быть положительной')
        this.#balance += amount
        this.#transactions.push({
          type: 'deposit',
          amount,
          balance: this.#balance,
          date: new Date().toLocaleDateString('ru-RU'),
        })
        return this  // для цепочки вызовов
      }
    
      withdraw(amount) {
        if (amount <= 0) throw new Error('Сумма снятия должна быть положительной')
        if (amount > this.#balance) throw new Error(`Недостаточно средств. Доступно: ${this.#balance} руб.`)
        this.#balance -= amount
        this.#transactions.push({
          type: 'withdraw',
          amount,
          balance: this.#balance,
          date: new Date().toLocaleDateString('ru-RU'),
        })
        return this
      }
    
      get balance() { return this.#balance }
      get owner() { return this.#owner }
      get history() { return [...this.#transactions] }
    
      toString() {
        return `Счёт [${this.#owner}]: ${this.#balance} руб.`
      }
    }
    
    const account = new BankAccount('Иван Петров', 10000)
    
    account.deposit(5000).deposit(2000)  // цепочка вызовов
    account.withdraw(3500)
    
    console.log(account.balance)         // 13500
    console.log(String(account))         // 'Счёт [Иван Петров]: 13500 руб.'
    console.log(account.history.length)  // 3
    
    try {
      account.withdraw(99999)
    } catch (e) {
      console.log(e.message)  // 'Недостаточно средств. Доступно: 13500 руб.'
    }
    
    // Прямой доступ к приватным полям невозможен
    // account.#balance = 999999  // SyntaxError

    Приватные и защищённые поля классов

    В финансовом приложении класс BankAccount хранит баланс. Если разработчик случайно сделает account.balance = 99999 — деньги появятся из ниоткуда. Приватные поля #balance делают это физически невозможным: попытка доступа снаружи класса — синтаксическая ошибка ещё до запуска кода.

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

  • «Классы» — синтаксис class, constructor, методы
  • «Наследование» — extends, super; приватные поля не наследуются
  • «Геттеры/сеттеры» — get/set для управляемого доступа к полям
  • Соглашение _protected (конвенция)

    Поле с префиксом _ — это соглашение между разработчиками: "не трогай снаружи". Технически оно доступно откуда угодно:

    class Database {
      constructor(url) {
        this._connectionString = url  // "защищено" по соглашению
        this._isConnected = false
      }
    
      connect() {
        this._isConnected = true
        console.log('Подключено к', this._connectionString)
      }
    }
    
    const db = new Database('postgres://localhost:5432/mydb')
    // Технически можно обратиться — защита только на уровне договорённости:
    console.log(db._connectionString)  // 'postgres://localhost:5432/mydb'

    Настоящие приватные поля # (ES2022)

    Поля с # — это настоящая приватность на уровне языка. Обратиться к ним снаружи класса невозможно — будет ошибка:

    class BankAccount {
      #balance = 0
      #transactions = []
    
      constructor(initialBalance) {
        this.#balance = initialBalance
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма должна быть положительной')
        this.#balance += amount
        this.#transactions.push({ type: 'deposit', amount, date: new Date() })
      }
    
      withdraw(amount) {
        if (amount > this.#balance) throw new Error('Недостаточно средств')
        this.#balance -= amount
        this.#transactions.push({ type: 'withdraw', amount, date: new Date() })
      }
    
      get balance() {
        return this.#balance  // геттер — readonly доступ снаружи
      }
    
      get history() {
        return [...this.#transactions]  // копия — снаружи нельзя изменить оригинал
      }
    }
    
    const acc = new BankAccount(5000)
    acc.deposit(2000)
    acc.withdraw(500)
    console.log(acc.balance)     // 6500 — через геттер
    // acc.#balance              // SyntaxError — нет доступа!

    Приватные методы #method()

    class PaymentProcessor {
      #apiKey
    
      constructor(apiKey) {
        this.#apiKey = apiKey
      }
    
      #validateAmount(amount) {  // приватный метод — только для внутреннего использования
        if (amount <= 0) throw new Error('Некорректная сумма')
        if (amount > 1_000_000) throw new Error('Превышен лимит')
      }
    
      charge(amount) {
        this.#validateAmount(amount)  // публичный метод вызывает приватный
        console.log(`Списание ${amount} руб. с ключом ${this.#apiKey.slice(0, 4)}...`)
      }
    }

    Статические приватные поля

    class IdGenerator {
      static #count = 0  // общий счётчик для всех экземпляров
    
      static next() {
        return ++IdGenerator.#count
      }
    
      static get total() {
        return IdGenerator.#count
      }
    }
    
    IdGenerator.next()  // 1
    IdGenerator.next()  // 2
    console.log(IdGenerator.total)  // 2
    // IdGenerator.#count  // SyntaxError

    Сравнение # и _ подходов

    | Критерий | _ (конвенция) | # (ES2022) |

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

    | Реальная защита | Нет | Да |

    | Доступ в подклассах | Да | Нет (нужен геттер/метод) |

    | Поддержка браузеров | Все | Современные (2021+) |

    | TypeScript | private / protected | Полная поддержка |

    Геттер для readonly доступа

    Приватное поле + геттер без сеттера = читаемое, но не изменяемое извне свойство:

    class Order {
      #id
      #status = 'pending'
    
      constructor() {
        this.#id = Math.random().toString(36).slice(2).toUpperCase()
      }
    
      get id() { return this.#id }
      get status() { return this.#status }
    
      confirm() { this.#status = 'confirmed' }
      cancel()  { this.#status = 'cancelled' }
      // Сеттера нет — статус меняется только через методы
    }

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

    1. Доступ к приватному полю из подкласса:

    class Animal {
      #name
    
      constructor(name) { this.#name = name }
      get name() { return this.#name }
    }
    
    class Dog extends Animal {
      bark() {
        // return this.#name  // SyntaxError! # поля не наследуются
        return this.name     // Правильно: через геттер родителя
      }
    }

    2. Попытка сериализации объекта с приватными полями через JSON.stringify:

    class User {
      #password
      name
    
      constructor(name, password) {
        this.name = name
        this.#password = password
      }
    }
    
    const user = new User('Иван', 'secret123')
    console.log(JSON.stringify(user))  // '{"name":"Иван"}' — #password не включается, это хорошо!
    // Но если нужно больше полей в JSON — добавь toJSON():
    // toJSON() { return { name: this.name, id: this.#id } }

    3. Использование # в коде задания с target ES2017 — ошибка компиляции в старых средах:

    // В учебных средах с ограниченным ES target
    // # поля могут не поддерживаться — используй _prefix конвенцию
    // В продакшн с современным target — # всегда предпочтительнее

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

  • Финансовые сервисы — баланс, история транзакций, PIN-коды
  • Аутентификация — хэш пароля, токены сессий
  • Кэши и пулы соединений — внутренний state не должен быть доступен потребителю
  • Счётчики и генераторы ID — статические приватные поля
  • Примеры

    Класс BankAccount с приватными полями #balance и #transactions, геттерами и цепочкой вызовов

    class BankAccount {
      #balance
      #transactions
      #owner
    
      constructor(owner, initialBalance = 0) {
        this.#owner = owner
        this.#balance = initialBalance
        this.#transactions = []
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма пополнения должна быть положительной')
        this.#balance += amount
        this.#transactions.push({
          type: 'deposit',
          amount,
          balance: this.#balance,
          date: new Date().toLocaleDateString('ru-RU'),
        })
        return this  // для цепочки вызовов
      }
    
      withdraw(amount) {
        if (amount <= 0) throw new Error('Сумма снятия должна быть положительной')
        if (amount > this.#balance) throw new Error(`Недостаточно средств. Доступно: ${this.#balance} руб.`)
        this.#balance -= amount
        this.#transactions.push({
          type: 'withdraw',
          amount,
          balance: this.#balance,
          date: new Date().toLocaleDateString('ru-RU'),
        })
        return this
      }
    
      get balance() { return this.#balance }
      get owner() { return this.#owner }
      get history() { return [...this.#transactions] }
    
      toString() {
        return `Счёт [${this.#owner}]: ${this.#balance} руб.`
      }
    }
    
    const account = new BankAccount('Иван Петров', 10000)
    
    account.deposit(5000).deposit(2000)  // цепочка вызовов
    account.withdraw(3500)
    
    console.log(account.balance)         // 13500
    console.log(String(account))         // 'Счёт [Иван Петров]: 13500 руб.'
    console.log(account.history.length)  // 3
    
    try {
      account.withdraw(99999)
    } catch (e) {
      console.log(e.message)  // 'Недостаточно средств. Доступно: 13500 руб.'
    }
    
    // Прямой доступ к приватным полям невозможен
    // account.#balance = 999999  // SyntaxError

    Задание

    В системе авторизации нужен класс Password. Реализуй: метод validate(input) — возвращает true если input совпадает с паролем; геттер strength — возвращает "weak" (длина < 6), "medium" (6-11 символов), "strong" (12+ символов и содержит цифры). Примечание: в starterCode используй обычное свойство _value вместо #value для совместимости с sandbox.

    Подсказка

    this._value = value в конструкторе. validate: return this._value === input. strength: проверь длину через .length и наличие цифры через /\d/.test(this._value). strong требует length >= 12 И наличие цифры (hasDigit).

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