Замыкания (Closures) в JavaScript — замыкания JavaScript
Что такое замыкание?
Замыкание (Closure) — это функция, которая имеет доступ к переменным из своей внешней (родительской) области видимости, даже после того, как эта внешняя функция завершила выполнение.
Простыми словами
Замыкание позволяет функции «запомнить» окружение, в котором она была создана, и использовать переменные из этого окружения позже.
Базовый пример
function outer() {
let counter = 0; // Переменная внешней функции
function inner() {
counter++; // Доступ к переменной внешней функции
console.log(counter);
}
return inner;
}
const increment = outer();
increment(); // 1
increment(); // 2
increment(); // 3
Что происходит?
- Функция
outerсоздаёт переменнуюcounterи функциюinner - Функция
innerвозвращается и присваивается вincrement - Хотя
outerзавершила выполнение,innerсохраняет доступ кcounter - При каждом вызове
increment()используется одна и та же переменнаяcounter
Ключевой момент:
Замыкание создаётся автоматически каждый раз, когда функция создаётся внутри другой функции и имеет доступ к её переменным.
Как работают замыкания?
Замыкания работают благодаря лексическому окружению (Lexical Environment).
Цепочка областей видимости
let global = 'Global';
function outer() {
let outerVar = 'Outer';
function inner() {
let innerVar = 'Inner';
console.log(innerVar); // Доступ к innerVar
console.log(outerVar); // Доступ к outerVar (замыкание)
console.log(global); // Доступ к global
}
return inner;
}
const closure = outer();
closure();
Практические примеры
Счётчик с приватными данными
function createCounter() {
let count = 0; // Приватная переменная
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// Нет прямого доступа к count
console.log(counter.count); // undefined
Инкапсуляция:
Замыкания позволяют создавать приватные переменные в JavaScript, которые нельзя изменить напрямую снаружи.
Функция-фабрика
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Каждый вызов createMultiplier создаёт своё собственное замыкание со своим значением multiplier.
Event listeners
function setupButtons() {
const buttons = document.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', function() {
console.log(`Кнопка ${index} нажата`); // Замыкание на index
});
});
}
Каждый обработчик события создаёт замыкание, которое запоминает свой index.
Классическая проблема с циклами
Проблема
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Выведет: 3, 3, 3 (а не 0, 1, 2)
Почему? Все три функции используют одно и то же замыкание с одной переменной i. К моменту выполнения setTimeout, цикл завершён и i = 3.
Решение 1: Использовать let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Выведет: 0, 1, 2
let создаёт новую переменную на каждой итерации цикла.
Решение 2: IIFE (до ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Выведет: 0, 1, 2
IIFE создаёт новую область видимости с собственной копией i.
Паттерны с замыканиями
Модуль (Module Pattern)
const Calculator = (function() {
// Приватные переменные и функции
let result = 0;
function log(operation, value) {
console.log(`${operation}: ${value}, result = ${result}`);
}
// Публичный API
return {
add: function(value) {
result += value;
log('Add', value);
return this;
},
subtract: function(value) {
result -= value;
log('Subtract', value);
return this;
},
getResult: function() {
return result;
}
};
})();
Calculator
.add(10)
.add(5)
.subtract(3);
console.log(Calculator.getResult()); // 12
Мемоизация (Memoization)
function memoize(fn) {
const cache = {}; // Приватный кэш
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('Из кэша');
return cache[key];
}
console.log('Вычисляем');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveOperation = memoize((n) => {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
return sum;
});
console.log(expensiveOperation(1000000)); // Вычисляем
console.log(expensiveOperation(1000000)); // Из кэша
Каррирование (Currying)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
React и замыкания
Проблема "устаревшего замыкания"
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// Всегда будет использовать начальное значение count = 0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // Пустой массив зависимостей
return <div>{count}</div>;
}
Решение
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// Использует актуальное значение
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
Утечки памяти
Замыкания могут привести к утечкам памяти, если они удерживают большие объекты.
Утечка памяти
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill('data');
return function() {
console.log('Hello');
// Функция не использует hugeArray, но он остаётся в памяти
};
}
const fn = createHeavyClosure();
Решение
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill('data');
// Используем только то, что нужно
const dataLength = hugeArray.length;
return function() {
console.log('Array length:', dataLength);
// hugeArray может быть удалён сборщиком мусора
};
}
Часто задаваемые вопросы
Что выведет этот код?
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // ?
funcs[1](); // ?
funcs[2](); // ?
Ответ: Все три вызова выведут 3, потому что все функции замыкаются на одной переменной i, которая после цикла равна 3.
Вывод
Замыкания — это:
- Функция + её лексическое окружение
- Доступ к переменным внешней функции после её завершения
- Основа для модулей, каррирования, мемоизации
- Способ создания приватных данных
- Потенциальный источник утечек памяти
- Причина "устаревших" значений в React хуках
На собеседовании:
Будьте готовы:
- Объяснить, что такое замыкание простыми словами
- Привести практические примеры использования
- Решить задачи с циклами и setTimeout
- Объяснить проблему утечек памяти
- Показать, как работают замыкания в React