# Closures, scope і hoisting: де помиляються кандидати

> Замикання в циклах, var проти let, hoisting функцій і змінних, temporal dead zone – з робочим кодом і чотирма питаннями-пастками, на яких реально «сипляться» на JavaScript-співбесідах.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-09-24
- Категорія: JavaScript і TypeScript
- Canonical: https://cookiesoftware.io/blog/closures-scope-hoisting

---

**Коротка відповідь.** Scope визначає, звідки видно змінну; замикання – це функція, яка запам'ятала свій лексичний scope і носить його з собою; hoisting – механізм, через який оголошення «спливають» на початок області видимості ще до виконання. Три теми звучать академічно, але на співбесіді перевіряються трьома-чотирма конкретними пастками. Розберімо саме їх.

## Scope за дві хвилини

У сучасному JavaScript три види області видимості: глобальна, функціональна та блокова. `let` і `const` мають блоковий scope, `var` – лише функціональний:

```js
function demo() {
  if (true) {
    var a = 1;
    let b = 2;
  }
  console.log(a); // 1 – var «не бачить» блоку
  console.log(b); // ReferenceError – let живе лише в блоці
}
```

Scope визначається місцем **написання** коду (лексично), а не місцем виклику. Це речення варто сказати на співбесіді дослівно – воно відрізняє розуміння від переказу.

## Пастка 1: замикання в циклі

Найчастіше питання по темі за всі часи:

```js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// 0, 1, 2
```

Чому так: із `var` існує **одна** змінна `i` на всі ітерації; колбеки виконаються після завершення циклу (див. [розбір event loop](/blog/event-loop-javascript)) і всі побачать фінальне значення 3. Із `let` кожна ітерація створює **нову прив'язку** змінної, і кожне замикання захоплює свою.

Сильна відповідь не закінчується на «треба let»: назви й старі обхідні шляхи (IIFE, третій аргумент setTimeout) – це показує, що ти розумієш механізм, а не запам'ятав виправлення.

## Пастка 2: hoisting функцій проти змінних

```js
sayHi();        // 'Привіт' – function declaration піднімається цілком
console.log(x); // undefined – var піднімається БЕЗ значення
// sayBye();    // TypeError: sayBye is not a function

function sayHi() {
  console.log('Привіт');
}
var x = 10;
var sayBye = function () {
  console.log('Бувай');
};
```

Правило: function declaration піднімається разом із тілом; `var` – лише оголошення (значення буде `undefined` до рядка присвоєння); function expression поводиться як звичайна змінна. Тому виклик `sayBye()` до присвоєння – це виклик `undefined`, звідси TypeError, а не ReferenceError. Різниця між цими двома помилками – окреме улюблене уточнення інтерв'юерів.

## Пастка 3: temporal dead zone

`let`/`const` теж «hoisted», але звертатися до них до рядка оголошення не можна – це і є TDZ:

```js
{
  // TDZ для value починається тут
  // console.log(value); // ReferenceError: Cannot access before initialization
  let value = 42;       // TDZ закінчується
  console.log(value);   // 42
}
```

Формулювання для співбесіди: «змінна існує з початку блоку, але недоступна до ініціалізації». Це пояснює, чому помилка каже саме *cannot access before initialization*, а не *is not defined*. Довідка з деталями – [MDN, розділ JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript).

## Замикання на практиці: once і memoize

Щоб тема перестала бути абстракцією, напиши дві утиліти, які реально живуть у продакшн-коді:

```ts
function once<T extends (...args: unknown[]) => unknown>(fn: T): T {
  let called = false;
  let result: unknown;
  return function (this: unknown, ...args: unknown[]) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  } as T;
}

function memoize(fn: (n: number) => number): (n: number) => number {
  const cache = new Map<number, number>();
  return (n) => {
    if (!cache.has(n)) cache.set(n, fn(n));
    return cache.get(n)!;
  };
}
```

Обидві працюють тільки завдяки замиканню: `called`, `result` і `cache` живуть між викликами, але недоступні ззовні. Питання «де замикання корисні в реальному коді» – саме про це: приватний стан, кешування, фабрики обробників подій. Побічний ефект, про який варто згадати: замикання тримає посилання на своє оточення, тому великі об'єкти в захопленому scope не звільняться збирачем сміття, поки живе функція.

## Замикання і пам'ять: про що питають на Middle+

Замикання тримає посилання на все своє лексичне оточення. Звідси реальний клас проблем:

```js
function attachHandler(element) {
  const bigData = new Array(1_000_000).fill('…'); // великий об'єкт
  element.addEventListener('click', () => {
    console.log('клік'); // bigData тут не потрібен…
  });
  // …але поки живе обробник, оточення (разом із bigData)
  // не може бути звільнене збирачем сміття
}
```

Виправлення очевидні, щойно бачиш причину: не тримати великі структури в scope обробника, або занулити посилання після використання, або знімати обробник (`removeEventListener`), коли елемент більше не потрібен. На співбесіді це питання звучить як «чи можуть замикання спричиняти витоки пам'яті» – правильна відповідь: самі по собі ні, але довгоживуча функція + великий захоплений scope = об'єкти, які не звільняються довше, ніж ти очікуєш.

## Чотири питання-пастки з відповідями

1. **«Що виведе цикл із var і setTimeout?»** – `3, 3, 3`; одна змінна на всі колбеки, виконання після циклу.
2. **«Чим ReferenceError відрізняється від TypeError у прикладах вище?»** – ReferenceError: звернення до неіснуючої/TDZ-змінної; TypeError: змінна існує, але з нею роблять неможливе (викликають undefined).
3. **«Чи піднімаються let і const?»** – так, але з TDZ: доступ до ініціалізації заборонений.
4. **«Як зробити лічильник, який не можна зламати ззовні?»** – функція-фабрика, що повертає методи над локальною змінною (див. приклад із `once`); змінна недоступна інакше як через ці методи.

## Що робити далі

Перевір себе чесно: закрий статтю і напиши з нуля `memoize` та цикл, що виводить `0, 1, 2` трьома різними способами. Якщо вийшло – тема твоя; якщо ні, повернись до відповідного розділу. Ці ж механіки лежать в основі половини задач із [добірки live coding](/blog/javascript-live-coding-zadachi), а місце теми в загальній картині співбесіди – у [великому розборі питань з JavaScript](/blog/javascript-interview-guide).
