Замыкание (closure) в JavaScript — это функция, которая имеет доступ к переменным из внешней области видимости даже после того, как внешняя функция завершила выполнение. Замыкание “запоминает” окружение, в котором оно было создано.
function outerFunction(x) {
// Внешняя переменная
return function innerFunction(y) {
return x + y; // Доступ к x из внешней области
};
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это позволяет функции получать доступ к переменным из внешней области видимости.
Ключевые особенности замыканий:
function createGreeting(name) {
const greeting = `Привет, ${name}!`;
return function() {
console.log(greeting); // Доступ к greeting из внешней области
};
}
const greetAlex = createGreeting("Александр");
const greetMaria = createGreeting("Мария");
greetAlex(); // "Привет, Александр!"
greetMaria(); // "Привет, Мария!"
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const square = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(square(5)); // 20
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count,
reset: () => count = initialValue
};
}
const counter1 = createCounter();
const counter2 = createCounter(10);
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 11
console.log(counter1.getValue()); // 2
console.log(counter2.getValue()); // 11
function createBankAccount(initialBalance) {
let balance = initialBalance;
let transactionHistory = [];
function addTransaction(type, amount) {
transactionHistory.push({
type,
amount,
date: new Date(),
balance: balance
});
}
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
addTransaction('deposit', amount);
return balance;
}
throw new Error('Сумма должна быть положительной');
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
addTransaction('withdraw', amount);
return balance;
}
throw new Error('Недостаточно средств или неверная сумма');
},
getBalance() {
return balance;
},
getHistory() {
return [...transactionHistory]; // Возвращаем копию
}
};
}
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
// balance недоступен напрямую!
// console.log(account.balance); // undefined
// НЕПРАВИЛЬНО - все функции выведут 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}
// Решение 1: Использование let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// Решение 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // 0, 1, 2
}, 100);
})(i);
}
// Решение 3: Функция-фабрика
function createLogger(value) {
return function() {
console.log(value);
};
}
for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 100); // 0, 1, 2
}
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Из кеша:', key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
console.log('Вычислено:', key);
return result;
};
}
// Медленная функция для демонстрации
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // Вычислено: [10]
console.log(memoizedFib(10)); // Из кеша: [10]
const Calculator = (function() {
let history = [];
let currentValue = 0;
function addToHistory(operation, value, result) {
history.push({
operation,
value,
result,
timestamp: Date.now()
});
}
return {
add(value) {
const result = currentValue + value;
addToHistory('add', value, result);
currentValue = result;
return this; // Для цепочки вызовов
},
subtract(value) {
const result = currentValue - value;
addToHistory('subtract', value, result);
currentValue = result;
return this;
},
multiply(value) {
const result = currentValue * value;
addToHistory('multiply', value, result);
currentValue = result;
return this;
},
getValue() {
return currentValue;
},
getHistory() {
return [...history];
},
clear() {
currentValue = 0;
history = [];
return this;
}
};
})();
// Использование
Calculator
.add(10)
.multiply(2)
.subtract(5);
console.log(Calculator.getValue()); // 15
console.log(Calculator.getHistory());
function createApiClient(baseUrl, defaultHeaders = {}) {
const headers = { ...defaultHeaders };
function makeRequest(endpoint, options = {}) {
const url = `${baseUrl}${endpoint}`;
const requestHeaders = { ...headers, ...options.headers };
return fetch(url, {
...options,
headers: requestHeaders
});
}
return {
get(endpoint, options) {
return makeRequest(endpoint, { ...options, method: 'GET' });
},
post(endpoint, data, options) {
return makeRequest(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
},
setHeader(key, value) {
headers[key] = value;
},
removeHeader(key) {
delete headers[key];
}
};
}
const apiClient = createApiClient('https://api.example.com', {
'Authorization': 'Bearer token123'
});
apiClient.setHeader('X-Custom-Header', 'value');
// apiClient.get('/users');
// apiClient.post('/users', { name: 'John' });
// ПЛОХО - может привести к утечке памяти
function attachListeners() {
const largeData = new Array(1000000).fill('data');
document.getElementById('button').addEventListener('click', function() {
// Замыкание держит ссылку на largeData
console.log('Clicked!');
});
}
// ХОРОШО - избегаем ненужных ссылок
function attachListeners() {
const largeData = new Array(1000000).fill('data');
function handleClick() {
console.log('Clicked!');
}
document.getElementById('button').addEventListener('click', handleClick);
// Очищаем ссылку, если она больше не нужна
// largeData = null; // Если данные больше не нужны
}
// Создание функций вне цикла
function createHandler(index) {
return function() {
console.log(`Handler ${index}`);
};
}
// ПЛОХО - создаем функции в цикле
const handlers1 = [];
for (let i = 0; i < 1000; i++) {
handlers1.push(function() {
console.log(`Handler ${i}`);
});
}
// ЛУЧШЕ - переиспользуем функцию-фабрику
const handlers2 = [];
for (let i = 0; i < 1000; i++) {
handlers2.push(createHandler(i));
}
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
return i;
});
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // ?
console.log(funcs[1]()); // ?
console.log(funcs[2]()); // ?
3
, 3
, 3
Все функции ссылаются на одну и ту же переменную i
, которая после завершения цикла равна 3.
Исправление:
for (let i = 0; i < 3; i++) { // let вместо var
functions.push(function() {
return i;
});
}
// Создайте функцию, которая возвращает объект с методами
// start(), stop(), getTime()
// Таймер должен считать секунды
function createTimer() {
// Ваш код здесь
}
const timer = createTimer();
timer.start();
// Через 3 секунды
console.log(timer.getTime()); // ~3
timer.stop();
function createTimer() {
let startTime = null;
let elapsedTime = 0;
let isRunning = false;
let intervalId = null;
return {
start() {
if (!isRunning) {
startTime = Date.now() - elapsedTime;
isRunning = true;
}
},
stop() {
if (isRunning) {
elapsedTime = Date.now() - startTime;
isRunning = false;
}
},
getTime() {
if (isRunning) {
return Math.floor((Date.now() - startTime) / 1000);
}
return Math.floor(elapsedTime / 1000);
},
reset() {
startTime = null;
elapsedTime = 0;
isRunning = false;
}
};
}
// Создайте функцию, которая может быть вызвана только n раз
function createLimitedFunction(fn, limit) {
// Ваш код здесь
}
const limitedLog = createLimitedFunction(console.log, 3);
limitedLog('Вызов 1'); // Выведет
limitedLog('Вызов 2'); // Выведет
limitedLog('Вызов 3'); // Выведет
limitedLog('Вызов 4'); // Не выведет
function createLimitedFunction(fn, limit) {
let callCount = 0;
return function(...args) {
if (callCount < limit) {
callCount++;
return fn.apply(this, args);
}
console.log(`Функция может быть вызвана только ${limit} раз(а)`);
};
}
// Создайте функцию, которая кеширует результаты вычислений
function createCachedFunction(fn, maxCacheSize = 10) {
// Ваш код здесь
}
function expensiveOperation(n) {
console.log(`Вычисляем для ${n}`);
return n * n;
}
const cached = createCachedFunction(expensiveOperation, 3);
console.log(cached(5)); // Вычисляем для 5, возвращает 25
console.log(cached(5)); // Из кеша, возвращает 25
function createCachedFunction(fn, maxCacheSize = 10) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
// Если кеш переполнен, удаляем самый старый элемент
if (cache.size >= maxCacheSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, result);
return result;
};
}
function mystery() {
let x = 1;
function inner() {
console.log(x);
let x = 2;
}
inner();
}
mystery();
ReferenceError: Cannot access 'x' before initialization
Из-за hoisting переменная x
объявлена в функции inner
, но обращение к ней происходит до инициализации (temporal dead zone).
function createAsyncCounter() {
let count = 0;
return {
async increment(delay = 1000) {
await new Promise(resolve => setTimeout(resolve, delay));
return ++count;
},
async getCount() {
return count;
}
};
}
const asyncCounter = createAsyncCounter();
// Использование
(async () => {
console.log(await asyncCounter.increment()); // 1 (через 1 сек)
console.log(await asyncCounter.increment(500)); // 2 (через 0.5 сек)
console.log(await asyncCounter.getCount()); // 2
})();
function createSequenceGenerator(start = 0, step = 1) {
let current = start;
return function* () {
while (true) {
yield current;
current += step;
}
};
}
const evenNumbers = createSequenceGenerator(0, 2)();
const oddNumbers = createSequenceGenerator(1, 2)();
console.log(evenNumbers.next().value); // 0
console.log(evenNumbers.next().value); // 2
console.log(oddNumbers.next().value); // 1
console.log(oddNumbers.next().value); // 3
// ОШИБКА: все обработчики ссылаются на последнее значение
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert('Кнопка ' + i); // Всегда последний i
};
}
// ИСПРАВЛЕНИЕ: используем let или замыкание
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert('Кнопка ' + i); // Правильный i
};
}
// ПРОБЛЕМА: замыкание держит ссылку на большой объект
function createHandler(largeObject) {
return function(event) {
// Используем только одно свойство
console.log(largeObject.id);
};
}
// РЕШЕНИЕ: извлекаем только нужные данные
function createHandler(largeObject) {
const id = largeObject.id; // Копируем только нужное
return function(event) {
console.log(id);
};
}
// Что выведет?
function test() {
console.log(a); // ?
console.log(b); // ?
var a = 1;
let b = 2;
}
test();
// a: undefined (hoisting)
// b: ReferenceError (temporal dead zone)
// Хорошо: приватные данные
function createUser(name, email) {
// Приватные переменные
let _name = name;
let _email = email;
let _isActive = true;
return {
getName: () => _name,
getEmail: () => _email,
isActive: () => _isActive,
deactivate: () => _isActive = false,
// Валидация при изменении
setEmail: (newEmail) => {
if (newEmail.includes('@')) {
_email = newEmail;
} else {
throw new Error('Неверный email');
}
}
};
}
// Плохо: ненужное замыкание
function processArray(arr) {
return arr.map(function(item) {
return item * 2;
});
}
// Лучше: простая функция
function double(item) {
return item * 2;
}
function processArray(arr) {
return arr.map(double);
}
/**
* Создает функцию с ограничением частоты вызовов (throttle)
* @param {Function} fn - Функция для ограничения
* @param {number} delay - Задержка в миллисекундах
* @returns {Function} Ограниченная функция
*/
function createThrottledFunction(fn, delay) {
let lastCallTime = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
if (now - lastCallTime >= delay) {
lastCallTime = now;
fn.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastCallTime = Date.now();
timeoutId = null;
fn.apply(this, args);
}, delay - (now - lastCallTime));
}
};
}
Замыкания — это мощный механизм JavaScript, который позволяет:
✅ Создавать приватные переменные и методы
✅ Сохранять состояние между вызовами функций
✅ Реализовывать паттерны вроде модуля и фабрики
✅ Создавать специализированные функции (каррирование, мемоизация)
✅ Управлять областью видимости переменных
Важно помнить:
let
и const
ведут себя по-разному в циклахСовременные альтернативы:
Замыкания — это основа понимания JavaScript. Изучите их хорошо, и вы сможете писать более элегантный и функциональный код!
Хочешь больше статей по подготовке к собеседованию? Подпишись на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляй сайт в избранное и прокачивай себя каждый день 💪