Skip to main content
Практика завдань

Замикання в JavaScript

Що таке Замикання?

Замикання — це функція, яка має доступ до змінних з її зовнішньої (батьківської) області видимості, навіть після того, як ця зовнішня функція завершила виконання.

Простими словами

Замикання дозволяє функції "пам'ятати" середовище, в якому вона була створена, і використовувати змінні з цього середовища пізніше.


Основний приклад

javascript
function outer() { let counter = 0; // Змінна з зовнішньої функції function inner() { counter++; // Доступ до змінної зовнішньої функції console.log(counter); } return inner; } const increment = outer(); increment(); // 1 increment(); // 2 increment(); // 3

Що відбувається?

  1. Функція outer створює змінну counter і функцію inner
  2. Функція inner повертається і присвоюється increment
  3. Хоча outer завершила виконання, inner зберігає доступ до counter
  4. Кожен виклик increment() використовує ту ж саму змінну counter

Ключовий момент:

Замикання створюється автоматично щоразу, коли функція створюється всередині іншої функції і має доступ до її змінних.


Як працюють Замикання?

Замикання працюють завдяки Лексичному середовищу.

Ланцюг областей видимості

javascript
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


Практичні приклади

Лічильник з приватними даними

javascript
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, які не можуть бути змінені безпосередньо ззовні.

Фабрика функцій

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.

Обробники подій

javascript
function setupButtons() { const buttons = document.querySelectorAll('button'); buttons.forEach((button, index) => { button.addEventListener('click', function() { console.log(`Кнопка ${index} натиснута`); // Замикання на index }); }); }

Кожен обробник подій створює замикання, яке пам'ятає свій index.


Класична проблема з циклами

Проблема

javascript
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

javascript
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 1000); } // Виводить: 0, 1, 2

let створює нову змінну на кожній ітерації циклу.

Рішення 2: IIFE (до ES6)

javascript
for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); }, 1000); })(i); } // Виводить: 0, 1, 2

IIFE створює нову область видимості з власною копією i.


Патерни з Замиканнями

Модульний патерн

javascript
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

Мемоізація

javascript
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)); // З кешу

Каррінг

javascript
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 та Замикання

Проблема "Застарілого Замикання"

javascript
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { // Завжди використовує початкове значення count = 0 setCount(count + 1); }, 1000); return () => clearInterval(timer); }, []); // Порожній масив залежностей return <div>{count}</div>; }

Рішення

javascript
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { // Використовує поточне значення setCount(prevCount => prevCount + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }

Витоки пам'яті

Замикання можуть призводити до витоків пам'яті, якщо вони утримують великі об'єкти.

Витік пам'яті

javascript
function createHeavyClosure() { const hugeArray = new Array(1000000).fill('data'); return function() { console.log('Привіт'); // Функція не використовує hugeArray, але він залишається в пам'яті }; } const fn = createHeavyClosure();

Рішення

javascript
function createHeavyClosure() { const hugeArray = new Array(1000000).fill('data'); // Використовувати лише те, що потрібно const dataLength = hugeArray.length; return function() { console.log('Довжина масиву:', dataLength); // hugeArray може бути зібрано сміттям }; }

Часто задавані питання

Що виведе цей код?

javascript
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

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?
Практика завдань