Комментарии (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 Protection | TypeScript | Наследование | Сложность |
|---|---|---|---|---|
| private (TS) | Нет | Да | Нет | Low |
| protected (TS) | Нет | Да | Да | Low |
| # fields (JS) | Да | Да | Нет | Low |
| WeakMap | Да | Нет | Нет | High |
| Closures | Да | Нет | Нет | Medium |
| Getters/Setters | Partial | Да | Да | 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
- Используй # fields в современном JavaScript — настоящее скрытие
- Используй private в TypeScript — для типизации
- Используй protected для подклассов — если нужно наследование
- Используй readonly для неизменяемых данных — безопаснее
- Используй interfaces для определения публичного контракта
- Не переусложняй — не все нужно скрывать
На 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.