Приватность через замыкания
Приватность через замыкания — техника инкапсуляции данных, при которой переменные объявляются в теле функции и недоступны снаружи, но доступны через возвращаемые публичные методы.
Зачем нужно
До появления приватных полей классов (#field, ES2022) замыкание было единственным надёжным способом создать по-настоящему приватные данные в JavaScript. Даже сейчас эта техника актуальна для функционального стиля и фабричных функций, где требуется инкапсуляция без классов.
Где используется
- Фабричные функции (factory functions) вместо классов
- Модульный паттерн (Module Pattern)
- Счётчики, кеши, очереди с ограниченным доступом
- Хранение токенов, сессий с контролем доступа
Базовый паттерн
function createCounter(initial = 0) {
// count — приватная переменная, недоступна снаружи
let count = initial;
return {
// Публичные методы — замыкают count
increment { return ++count; },
decrement { return --count; },
getCount { return count; },
reset { count = initial; }
};
}
const counter = createCounter(10);
counter.increment; // 11
counter.increment; // 12
counter.decrement; // 11
// Прямой доступ недоступен
console.log(counter.count); // undefined
counter.count = 999; // не изменит внутреннее count!
console.log(counter.getCount); // 11
Модульный паттерн (IIFE)
const UserStore = (function {
// Приватное состояние
const users = new Map();
let nextId = 1;
// Приватные вспомогательные функции
function validate(user) {
return user.name && user.email;
}
// Публичный API
return {
add(userData) {
if (!validate(userData)) throw new Error('Невалидные данные');
const id = nextId++;
users.set(id, { ...userData, id });
return id;
},
get(id) {
return users.get(id) || null;
},
getAll {
return [...users.values()]; // копия, не оригинал
},
count {
return users.size;
}
};
});
UserStore.add({ name: 'Иван', email: 'ivan@mail.ru' });
UserStore.add({ name: 'Анна', email: 'anna@mail.ru' });
console.log(UserStore.count); // 2
console.log(UserStore.get(1)); // { name: 'Иван', email: '...', id: 1 }
// Приватные данные недоступны
console.log(UserStore.users); // undefined
Геттеры и сеттеры с валидацией
function createPerson(initialName, initialAge) {
let name = initialName;
let age = initialAge;
return {
getName { return name; },
setName(newName) {
if (typeof newName !== 'string' || !newName.trim()) {
throw new Error('Имя должно быть непустой строкой');
}
name = newName.trim();
},
getAge { return age; },
setAge(newAge) {
if (!Number.isInteger(newAge) || newAge < 0 || newAge > 150) {
throw new Error('Некорректный возраст');
}
age = newAge;
},
toString {
return `${name}, ${age} лет`;
}
};
}
const person = createPerson('Иван', 25);
person.setName('Анна');
person.setAge(30);
console.log(person.toString()); // Анна, 30 лет
person.setAge(-5); // Error: Некорректный возраст
Сравнение с приватными полями классов (ES2022)
// Замыкание — приватность через функциональный подход
function createBankAccount(balance) {
let _balance = balance;
return {
deposit(n) { _balance += n; },
getBalance { return _balance; }
};
}
// Приватные поля класса — синтаксическая поддержка в JS
class BankAccount {
#balance; // по-настоящему приватное поле
constructor(balance) { this.#balance = balance; }
deposit(n) { this.#balance += n; }
getBalance { return this.#balance; }
}
// Разница: в factory function каждый экземпляр создаёт новые методы
// В классе методы живут на прототипе — эффективнее при большом числе объектов
Частые ошибки
1. Возврат примитива вместо объекта теряет доступ к методам
function makeValue(initial) {
let val = initial;
return val; // возвращает копию, не замыкание!
}
const v = makeValue(5);
// v — просто число, нет доступа к val
2. Мутация возвращённых данных нарушает инкапсуляцию
function createList() {
const items = ;
return {
getItems { return items; } // возвращаем ссылку!
};
}
const list = createList;
const leaked = list.getItems;
leaked.push('внешний элемент'); // мутируем внутренний массив!
// Решение: возвращать копию
getItems { return [...items]; }