Замыкание — это функция, которая «запоминает» переменные из внешней области видимости. Теорию знают все. А вот как она работает в коде с вложенными циклами, таймерами и замыканиями в цикле — это уже другой уровень.
Разбираем 12 задач, которые действительно проверяют понимание замыканий.
Замыкание (closure) — это функция, которая захватывает переменные из внешней области видимости и «помнит» их даже после того, как внешняя функция завершилась.
Каждый вызов функции создаёт лексическое окружение (lexical environment) — объект, в котором хранятся переменные этой функции и ссылка на лексическое окружение родителя.
function outer() {
const x = 10;
function inner() {
console.log(x); // x берётся из лексического окружения outer
}
inner();
}
outer();При вызове outer() создаётся окружение { x: 10, parent: globalEnv }. Когда inner ищет x, она смотрит в своё окружение (пустое), потом в outer (находит x = 10).
Поиск переменных идёт вверх по цепочке лексических окружений:
const global = "global";
function level1() {
const level1Var = "level1";
function level2() {
const level2Var = "level2";
function level3() {
console.log(global); // → globalEnv
console.log(level1Var); // → level1Env
console.log(level2Var); // → level2Env
}
level3();
}
level2();
}
level1();Каждая функция ссылается на окружение, в котором была создана, а не вызвана.
Замыкание создаётся в момент объявления функции, а не вызова:
function create() {
const secret = 42;
return function () {
return secret;
};
}
// Здесь secret уже «запомнена»
const fn = create();
// А вызываем мы функцию позже
fn(); // 42В момент return function () { ... } движок JavaScript связывает эту функцию с лексическим окружением create. Даже после завершения create окружение не удаляется — на него ссылается возвращённая функция.
Замыкание хранит ссылку на переменную, а не её копию:
function counter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const c = counter();
c.increment();
c.increment();
c.getCount(); // 2Оба метода (increment и getCount) ссылаются на одну и ту же переменную count. Изменяя count в increment, мы видим это в getCount.
Вот обратный пример — замыкание захватывает ссылку, а значение вычисляется позже:
function createFunctions() {
const arr = [];
for (var i = 0; i < 3; i++) {
arr.push(() => i);
}
return arr;
}
const [f0, f1, f2] = createFunctions();
f0(); // 3
f1(); // 3
f2(); // 3Все три функции ссылаются на одну i. К моменту вызова цикл завершился, и i === 3. Чтобы исправить — используйте let (создаёт новую переменную на каждой итерации) или IIFE (создаёт новое окружение).
Инкапсуляция — приватные переменные без классов:
function createBankAccount(initial) {
let balance = initial;
return {
deposit: (amount) => (balance += amount),
getBalance: () => balance,
};
}
const account = createBankAccount(100);
account.deposit(50);
account.getBalance(); // 150
account.balance; // undefinedФабрики функций — генерация функций с предустановленными параметрами:
function createMultiplier(n) {
return (x) => x * n;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
double(5); // 10
triple(5); // 15Колбэки и обработчики — функция «помнит» контекст:
function setupButton(name) {
const button = document.querySelector(`#${name}`);
button.addEventListener("click", () => {
console.log(`Button ${name} clicked`);
});
}
setupButton("submit"); // name запомнена в замыканииЧастичное применение — предустановка аргументов:
function log(level, message) {
console.log(`[${level}] ${message}`);
}
function createLogger(level) {
return (message) => log(level, message);
}
const errorLog = createLogger("ERROR");
errorLog("Something broke"); // [ERROR] Something brokeСоздайте функцию makeCounter, которая возвращает объект с методами increment, decrement и getCount. Счётчик начинается с 0.
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
};
}
const counter = makeCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
counter.getCount(); // 1Почему это работает: count живёт в области видимости makeCounter. После вызова makeCounter() функция завершается, но count не удаляется — на него ссылаются три метода через замыкание.
Добавьте параметр start в makeCounter. Счётчик начинается с start.
function makeCounter(start) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
};
}
const counter = makeCounter(10);
counter.increment(); // 11
counter.getCount(); // 11Ключевой момент: start — это параметр функции, он тоже попадает в замыкание. Но count — это отдельная переменная, которую можно менять независимо от start.
Что выведет этот код?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 3, 3, 3Почему: var создаёт одну переменную i на всю функцию. К моменту срабатывания setTimeout цикл уже завершился, и i === 3.
Теперь с let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 0, 1, 2Почему: let создаёт новую переменную i на каждой итерации. У каждого колбэка — своя копия i.
Создайте массив функций, каждая из которых возвращает свой индекс. Используя var.
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(() => i);
}
return functions;
}
const [f0, f1, f2] = createFunctions();
f0(); // 3
f1(); // 3
f2(); // 3Все три функции ссылаются на одну i, которая к моменту вызова равна 3.
Исправьте задачу 5, чтобы f0() возвращал 0, f1() — 1, f2() — 2. Три варианта:
Вариант 1 — IIFE:
for (var i = 0; i < 3; i++) {
functions.push(
(
(j) => () =>
j
)(i),
);
}Вариант 2 — bind:
functions.push((() => i).bind(null));
// Но bind не захватывает i по значению, это не сработает.
// Правильный bind-вариант:
for (var i = 0; i < 3; i++) {
functions.push(
(
(val) => () =>
val
)(i),
);
}Вариант 3 — let (самый простой):
for (let i = 0; i < 3; i++) {
functions.push(() => i);
}Создайте BankAccount с приватным балансом, методами deposit, withdraw и getBalance. Баланс нельзя изменить напрямую.
function BankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount > 0) balance += amount;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) balance -= amount;
},
getBalance() {
return balance;
},
};
}
const account = BankAccount(1000);
account.deposit(500);
account.withdraw(200);
account.getBalance(); // 1300
account.balance; // undefined — нельзя получить напрямуюЗачем: До классов с # приватными полями это был стандартный способ инкапсуляции. Замыкание изолирует balance от внешнего мира.
Создайте фабрику createMultiplier, которая возвращает функцию-умножитель.
function createMultiplier(multiplier) {
return (number) => number * multiplier;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
double(5); // 10
triple(5); // 15Паттерн: Замыкание как способ «запомнить» параметр. Функция double не знает про createMultiplier, но multiplier живёт внутри неё.
Что выведет этот код?
function createGreeter(greeting) {
return function (name) {
console.log(`${greeting}, ${name}!`);
};
}
const greetHello = createGreeter("Hello");
greetHello("Alice"); // "Hello, Alice!"А теперь с объектом:
const user = {
greeting: "Hi",
greet: createGreeter(this.greeting),
};
user.greet("Bob"); // "undefined, Bob!"Почему: this.greeting вычисляется в момент вызова createGreeter, когда this — глобальный объект (или undefined в strict mode). Замыкание запомнило undefined, а не "Hi".
Создайте функцию makeCounters(n), которая возвращает массив из n независимых счётчиков.
function makeCounters(n) {
const counters = [];
for (let i = 0; i < n; i++) {
let count = 0;
counters.push({
increment: () => ++count,
getCount: () => count,
});
}
return counters;
}
const [c1, c2, c3] = makeCounters(3);
c1.increment();
c1.increment();
c1.increment();
c2.increment();
c3.increment();
c3.increment();
c1.getCount(); // 3
c2.getCount(); // 1
c3.getCount(); // 2Каждый count — отдельная переменная в отдельном замыкании.
function createFetcher(url) {
return async () => {
const response = await fetch(url);
return response.json();
};
}
const fetchUsers = createFetcher("https://api.example.com/users");
const user = await fetchUsers();Что происходит: createFetcher возвращает функцию, которая захватывает url. Позже, при вызове, она использует этот url для запроса. Замыкание работает даже с async/await.
Реализуйте мемоизацию с помощью замыкания:
function memoize(fn) {
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);
cache.set(key, result);
return result;
};
}
const expensiveAdd = memoize((a, b) => {
console.log("Computing...");
return a + b;
});
expensiveAdd(1, 2); // "Computing..." → 3
expensiveAdd(1, 2); // 3 (без вычисления)Замыкание хранит cache — и внешний мир не может до него добраться.
Что выведет этот код?
function outer() {
let x = 10;
function inner() {
console.log(x);
}
x = 20;
return inner;
}
outer()(); // 20Почему: Замыкание захватывает ссылку на переменную, а не значение. Когда inner вызывается, x уже равно 20.
function createTask(name, delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => resolve(name), delay);
});
}
const task1 = createTask("first", 100);
const task2 = createTask("second", 50);
const results = await Promise.all([task1(), task2()]);
results; // ["first", "second"]Каждая задача «запомнила» своё имя и задержку.
var в цикле// Плохо
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// Выведет 5, 5, 5, 5, 5
// Хорошо
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// Выведет 0, 1, 2, 3, 4function createFunctions() {
const result = [];
for (let i = 0; i < 3; i++) {
let fn = () => i;
result.push(fn);
fn = null; // Перезаписали — но result уже хранит ссылку на fn
}
return result;
}
createFunctions()[0](); // 0 — работает, потому что result[0] ссылка на fnfunction createHeavy() {
const hugeArray = new Array(1000000).fill("data");
return () => hugeArray.length;
}
const fn = createHeavy(); // hugeArray живёт, пока жив fnЕсли fn больше не нужен — обнулите его: fn = null.
Замыкания — это один из тех концептов, которые «понятны в теории, но ломаются на практике». Попробуйте решить задачи на Девстанции — там есть десятки задач, которые проверяют, как вы понимаете область видимости и замыкания в реальных ситуациях.
А если готовитесь к собеседованию — посмотрите чеклист из 50 вопросов и 10 задач с разбором.