Skip to main content

Область видимості в JavaScript: типи та принципи роботи

Область видимості (scope) в JavaScript визначає, де змінну можна прочитати або змінити. Рушій вирішує scope під час компіляції, а не виконання. Тому це називають лексичною областю видимості.

Теорія

TL;DR

  • Scope = ділянка коду, де існує змінна і де до неї є доступ
  • Три типи: глобальна (весь код), функції (всередині однієї функції), блокова (всередині одного {})
  • var враховує тільки межі функції; let і const враховують межі блоку
  • Вкладені scope-и бачать змінні батьківських, але не навпаки
  • Правило вибору: оголошуй let/const у найменшому можливому блоці, уникай var

Швидкий приклад

javascript
const globalVar = 'global'; // глобальна область видимості function outer() { const outerVar = 'outer'; // область функції if (true) { let blockVar = 'block'; // блокова область console.log(globalVar); // "global" - читає вгору по ланцюжку console.log(outerVar); // "outer" - читає вгору по ланцюжку console.log(blockVar); // "block" - локальна } console.log(blockVar); // ReferenceError - блок завершився } outer(); console.log(outerVar); // ReferenceError - функція завершилась

Кожна scope може читати змінні з батьківської, але батьківська не дістається до дочірньої.

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

JavaScript використовує лексичну область видимості: вона визначається тим, де написаний код, а не де він запускається. Коли рушій шукає змінну, він починає з поточної scope і піднімається вгору по ланцюжку до глобальної, поки не знайде змінну або не кине ReferenceError.

V8 створює об'єкт LexicalEnvironment для кожної scope під час компіляції. Ці об'єкти пов'язані з батьківськими через зовнішнє посилання і утворюють ланцюжок. Пошук змінних під час виконання іде по цих посиланнях крок за кроком.

Коли що використовувати

  • Глобальна scope: константи для всього застосунку, наприклад process.env.NODE_ENV або спільний конфіг
  • Scope функції: ізолюй логіку всередині утиліти або обробника, щоб вона не впливала на інші функції
  • Блокова scope: обмеж змінну циклом for, блоком if або try/catch без витоку назовні

Чим вужча область видимості, тим менше несподіванок під час code review.

var, let і const

var прив'язується до найближчої функції (або глобальної scope, якщо поза всіма функціями). Блоки вона ігнорує повністю. let і const прив'язуються до найближчого блоку. Вони також підпадають під Temporal Dead Zone (TDZ): змінна існує в scope з початку блоку, але будь-який доступ до рядка оголошення кидає ReferenceError.

javascript
console.log(typeof a); // "undefined" - var підняте, ініціалізоване undefined console.log(typeof b); // ReferenceError - let у TDZ var a = 1; let b = 2;

Це збиває з пантелику навіть досвідчених розробників. Бачив на code review, як хтось вважав typeof завжди безпечним до рядка оголошення.

Типові помилки

var у циклі

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виводить: 3 3 3 }

Усі три колбеки замикаються на одному i, бо var має область видимості функції. Коли вони спрацьовують, цикл вже завершився і i дорівнює 3. Рішення: замінити на let, яка створює нове прив'язування для кожної ітерації.

Припущення, що var поважає блоки

javascript
if (true) { var x = 5; } console.log(x); // 5 - витікає з блоку

var зупиняється тільки на межах функції. Тут потрібна let або const.

Приховування (shadowing) без усвідомлення

javascript
const user = 'Alice'; function greet() { const user = 'Bob'; // приховує зовнішній user console.log(user); // "Bob" } greet(); console.log(user); // "Alice"

Shadowing - валідний JavaScript, але він ускладнює відстеження коду. Краще перейменуй внутрішню змінну.

Де це зустрічається

  • React: useEffect замикається на змінних scope компонента. Якщо users змінився, новий effect захоплює свіже посилання
  • Express: обробники маршрутів замикаються на об'єкті app зі scope модуля
  • Node.js: require загортає кожен модуль у функцію, даючи йому власну scope для exports
  • Redux thunks: захоплюють dispatch зі scope підключеного компонента

Питання на співбесіді

Q: Що виведе console.log(x); var x = 1;?
A: undefined. Оголошення піднімається на початок scope функції, але ініціалізація залишається на місці.

Q: Чому for (var i...) з setTimeout виводить одне число тричі?
A: var створює одне прив'язування, спільне для всіх ітерацій. Усі замикання вказують на той самий i. Заміни на let - кожна ітерація отримає власне прив'язування.

Q: Що таке Temporal Dead Zone?
A: TDZ - це проміжок між моментом, коли змінна let/const з'являється в scope (початок блоку), і рядком оголошення. Будь-який доступ у цьому проміжку кидає ReferenceError.

Q: Чи стає let на верхньому рівні глобальною змінною?
A: Ні. var x = 1 на верхньому рівні додає x до window у браузері. let x = 1 на верхньому рівні створює змінну з блоковою областю видимості, яка не є властивістю window.

Q: Як V8 оптимізує пошук змінних у гарячих циклах?
A: V8 використовує inline caching. Після першого пошуку рушій запам'ятовує тип і розташування змінної. Наступні звернення в тій самій scope обходять весь ланцюжок. Це одна з причин, чому вужча область видимості покращує продуктивність.

Приклади

Базовий: ланцюжок областей видимості

javascript
const language = 'JavaScript'; function describe() { const topic = 'scope'; function announce() { const detail = 'лексична'; console.log(`${detail} ${topic} у ${language}`); // "лексична scope у JavaScript" // Кожна змінна - з різного рівня вкладеності } announce(); } describe();

announce читає detail зі своєї scope, topic з батьківської функції і language з глобальної. Пошук завжди іде зсередини назовні, ніколи навпаки.

Середній рівень: var проти let у циклі

Класичне питання на JavaScript-співбесіді. Дві версії - два різних результати:

javascript
// var: усі замикання поділяють одне прив'язування for (var i = 0; i < 3; i++) { setTimeout(() => console.log('var:', i), 0); } // Виведе: var: 3 var: 3 var: 3 // let: кожна ітерація отримує власне прив'язування for (let j = 0; j < 3; j++) { setTimeout(() => console.log('let:', j), 0); } // Виведе: let: 0 let: 1 let: 2

Різниця повністю в правилах scope. var має область видимості функції, тому є один i для всього циклу. let має блокову область, тому кожне тіло циклу отримує свій j, який належить тільки цій ітерації.

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

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

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

Дочитали статтю?