← Назад к вопросам

Можно ли скрыть только поля при инкапсуляции?

2.2 Middle🔥 121 комментариев
#ООП

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Инкапсуляция и скрытие полей в TypeScript/JavaScript

Да, можно скрыть только поля. Вот подробный анализ разных способов инкапсуляции в JavaScript и TypeScript.

1. Private модификатор (TypeScript)

class User {
  // Только в TypeScript, не в JS runtime
  private password: string;
  public name: string;
  
  constructor(name: string, password: string) {
    this.name = name;
    this.password = password;
  }
  
  // Методы могут быть private
  private hashPassword(password: string): string {
    return 'hashed_' + password;
  }
  
  // Публичный метод для проверки пароля
  public checkPassword(password: string): boolean {
    return this.hashPassword(password) === this.password;
  }
}

const user = new User('John', '123456');
console.log(user.name);          // OK
// console.log(user.password);    // Ошибка TypeScript!
// user.hashPassword('123');      // Ошибка TypeScript!

Проблема: private работает только при компиляции TypeScript. В runtime JavaScript можно обойти:

// Скомпилированный JS:
class User {
  constructor(name, password) {
    this.name = name;
    this.password = password;  // Все равно доступно!
  }
}

const user = new User('John', '123');
console.log(user.password);  // Работает! private только для TS

2. Protected модификатор (TypeScript)

class Animal {
  protected name: string;  // Доступно в подклассах, но не снаружи
  
  constructor(name: string) {
    this.name = name;
  }
  
  protected makeSound(): void {
    console.log('Some sound');
  }
}

class Dog extends Animal {
  bark(): void {
    // Можу использовать protected свойства
    console.log(this.name + ' barks!');
    this.makeSound();
  }
}

const dog = new Dog('Buddy');
dog.bark();               // OK
// console.log(dog.name);  // Ошибка!

3. # Private fields (современный JavaScript)

Настоящее скрытие на уровне runtime:

class User {
  #password;  // Настоящее private поле JavaScript
  name;
  
  constructor(name, password) {
    this.name = name;
    this.#password = password;
  }
  
  checkPassword(password) {
    return this.#hashPassword(password) === this.#password;
  }
  
  #hashPassword(password) {
    return 'hashed_' + password;
  }
}

const user = new User('John', '123456');
console.log(user.name);        // OK
// console.log(user.#password); // Syntax Error! Нельзя даже обращаться
// user.#hashPassword();        // Syntax Error!

Это реальное скрытие, не обойти!

4. WeakMap для скрытия данных

const privateData = new WeakMap();

class User {
  constructor(name, password) {
    this.name = name;
    privateData.set(this, { password });
  }
  
  checkPassword(password) {
    const data = privateData.get(this);
    return this.#hashPassword(password) === data.password;
  }
  
  #hashPassword(password) {
    return 'hashed_' + password;
  }
}

const user = new User('John', '123456');
console.log(user.name);         // OK
console.log(privateData.get(user)); // Доступно, но это деталь реализации

5. Getters и Setters

class User {
  private _password: string;
  
  constructor(password: string) {
    this._password = this._hash(password);
  }
  
  // Скрыть логику валидации
  set password(newPassword: string) {
    if (newPassword.length < 8) {
      throw new Error('Password too short');
    }
    this._password = this._hash(newPassword);
  }
  
  // Скрыть внутренний формат
  get passwordLength(): number {
    return this._password.length;
  }
  
  private _hash(password: string): string {
    return 'hashed_' + password;
  }
}

const user = new User('password123');
user.password = 'newpass123';
console.log(user.passwordLength);  // OK

6. Closure (функциональный подход)

function createUser(name, password) {
  let _password = password;  // Скрыто в closure
  
  return {
    name,
    checkPassword: (pwd) => _password === pwd,
    setPassword: (newPwd) => { _password = newPwd; }
  };
}

const user = createUser('John', '123456');
console.log(user.name);           // OK
console.log(user.checkPassword('123456'));  // OK
// console.log(user._password);   // undefined, не доступно

7. Комбинированный подход (лучший)

class User {
  // Public
  readonly id: string;
  public name: string;
  
  // Private (скрыто в TypeScript и runtime)
  #password: string;
  #lastLogin?: Date;
  
  // Protected (для подклассов)
  protected createdAt: Date;
  
  constructor(id: string, name: string, password: string) {
    this.id = id;
    this.name = name;
    this.#password = password;
    this.createdAt = new Date();
  }
  
  // Public метод
  public authenticate(password: string): boolean {
    return this.#verifyPassword(password);
  }
  
  // Protected метод (для подклассов)
  protected updateLastLogin(): void {
    this.#lastLogin = new Date();
  }
  
  // Private методы (настоящее скрытие)
  #verifyPassword(password: string): boolean {
    return this.#hashPassword(password) === this.#password;
  }
  
  #hashPassword(password: string): string {
    return 'hash_' + password;
  }
}

class AdminUser extends User {
  banUser(userId: string): void {
    // Можем использовать protected updateLastLogin
    this.updateLastLogin();
    console.log(`User ${userId} banned by ${this.name}`);
  }
  
  // ❌ Не можем использовать #verifyPassword - только доступна в User
}

8. Interface для публичного API

// Публичный интерфейс (контракт для клиентов)
interface IUser {
  readonly id: string;
  readonly name: string;
  authenticate(password: string): boolean;
}

// Внутренняя реализация (может иметь приватные поля)
class User implements IUser {
  readonly id: string;
  readonly name: string;
  #password: string;
  
  constructor(id: string, name: string, password: string) {
    this.id = id;
    this.name = name;
    this.#password = password;
  }
  
  authenticate(password: string): boolean {
    return password === this.#password;
  }
  
  // Внутренние методы, не в интерфейсе
  private updateProfile(): void {}
}

// Клиент видит только IUser
const user: IUser = new User('1', 'John', '123');
// user.updateProfile();  // Ошибка! Нет в интерфейсе

9. Сравнение подходов

СпособRuntime ProtectionTypeScriptНаследованиеСложность
private (TS)НетДаНетLow
protected (TS)НетДаДаLow
# fields (JS)ДаДаНетLow
WeakMapДаНетНетHigh
ClosuresДаНетНетMedium
Getters/SettersPartialДаДаMedium

10. Практический пример: Database Connection Pool

class ConnectionPool {
  readonly maxConnections: number = 10;
  
  #connections: Map<string, Connection> = new Map();
  #activeConnections: Set<string> = new Set();
  
  public async getConnection(): Promise<Connection> {
    if (this.#activeConnections.size >= this.maxConnections) {
      throw new Error('No available connections');
    }
    
    const connId = this.#generateId();
    const conn = new Connection(connId);
    
    this.#connections.set(connId, conn);
    this.#activeConnections.add(connId);
    
    return conn;
  }
  
  public releaseConnection(conn: Connection): void {
    this.#activeConnections.delete(conn.id);
  }
  
  public getStats(): { total: number; active: number } {
    return {
      total: this.#connections.size,
      active: this.#activeConnections.size
    };
  }
  
  #generateId(): string {
    return 'conn_' + Date.now() + '_' + Math.random();
  }
  
  #cleanup(): void {
    // Внутренняя очистка
  }
}

const pool = new ConnectionPool();
const conn = await pool.getConnection();
console.log(pool.getStats());  // OK
// console.log(pool.#connections);  // Error! Не доступно

Best Practices

  1. Используй # fields в современном JavaScript — настоящее скрытие
  2. Используй private в TypeScript — для типизации
  3. Используй protected для подклассов — если нужно наследование
  4. Используй readonly для неизменяемых данных — безопаснее
  5. Используй interfaces для определения публичного контракта
  6. Не переусложняй — не все нужно скрывать

На production

В моих приложениях я использую стандартный паттерн:

// User domain model
class User {
  // Public
  readonly id: UserId;
  readonly email: Email;
  
  // Private (настоящее скрытие)
  #passwordHash: string;
  #salt: string;
  
  // Protected (для наследования)
  protected createdAt: Date;
  
  constructor(id: UserId, email: Email, passwordHash: string, salt: string) {
    this.id = id;
    this.email = email;
    this.#passwordHash = passwordHash;
    this.#salt = salt;
    this.createdAt = new Date();
  }
  
  public authenticate(password: string): boolean {
    return this.#verifyPassword(password);
  }
  
  #verifyPassword(password: string): boolean {
    // Implementation
    return true;
  }
}

Да, можно скрыть только поля. Это основная идея инкапсуляции — скрыть детали реализации, показать только нужный публичный API.