Замикання в JavaScript
Що таке Замикання?
Замикання — це функція, яка має доступ до змінних з її зовнішньої (батьківської) області видимості, навіть після того, як ця зовнішня функція завершила виконання.
Простими словами
Замикання дозволяє функції "пам'ятати" середовище, в якому вона була створена, і використовувати змінні з цього середовища пізніше.
Основний приклад
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
Ключовий момент:
Замикання створюється автоматично щоразу, коли функція створюється всередині іншої функції і має доступ до її змінних.
Як працюють Замикання?
Замикання працюють завдяки Лексичному середовищу.
Ланцюг областей видимості
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();closure
ланцюг областей видимості
Глобальна область: global
Зовнішня область: outerVar
Внутрішня область: innerVar
Практичні приклади
Лічильник з приватними даними
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.
Обробники подій
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, 2let створює нову змінну на кожній ітерації циклу.
Рішення 2: IIFE (до ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Виводить: 0, 1, 2IIFE створює нову область видимості з власною копією i.
Патерни з Замиканнями
Модульний патерн
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Мемоізація
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)); // З кешуКаррінг
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)); // 6React та Замикання
Проблема "Застарілого Замикання"
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('Привіт');
// Функція не використовує hugeArray, але він залишається в пам'яті
};
}
const fn = createHeavyClosure();Рішення
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill('data');
// Використовувати лише те, що потрібно
const dataLength = hugeArray.length;
return function() {
console.log('Довжина масиву:', 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
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.