Блог

Невидимі зони вередливої мови JavaScript, або Чому чимало розробників мають багато клопотів з ними

JavaScript — дуже гнучка та, з іншого боку, вередлива мова. Наприклад, всі ми чули про блочні області видимості, а ви знали, що багато розробників мають багато клопотів з ними? 

Дехто не визначає на особливості let та const і використовує їх бездумно, як var. Але тут потрібно бути обережним, використання деяких змінних викличе помилку ініціалізації. Така ситуація має назву Temporal Dead Zone.

Саме про неї сьогодні поговоримо.

JavaScript Hosting*

На перший погляд, термін «Тимчасова мертва зона» (Temporal Dead Zone) звучить досить загадково й здається, що ви з цим ще не зустрічалися, але насправді до мертвих зон наводять особливості декларації змінних у JavaScript, які корисно розуміти та з якими ви стикаєтеся кожен день.

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

Hoisting — не термін, його не визначено нормативно у специфікації ECMAScript. Специфікація визначає групу оголошень як HoistableDeclaration, але вона включає лише оголошення функцій, функцій*, асинхронних функцій й асинхронних функцій*. Hoisting також часто розглядається і як властивість оголошень var, але по-іншому.

Які дії можна розглядати як hoisting:

  • Можливість використовувати значення змінної в її області видимості до рядка її оголошення. («Value hoisting»).
  • Можливість посилатися на змінну в її області видимості до рядка, в якому вона оголошена, без викиду помилки ReferenceError, але значення завжди залишається невизначеним. («Declaration hoisting»)
  • Оголошення змінної викликає зміни поведінки в її області видимості до рядка, у якому вона оголошена.

Тепер можна переходити до ознайомлення з областями видимості.

Область видимості (Scope)

JavaScript — досить стара мова, і з віком приходить зрілість. Специфікація ECMAscript з часом вдосконалювалася й у випуску ES2015 (ES6) з’явилися нові фічі, які допомагають уникнути проблем із глобальним охопленням та покращити ваш код і водночас привносять мертві зони при неправильному використанні.

Раніше при декларації змінних використовувалося лише ключове слово var, а з ES6 було введено let і const.

Ключове слово const використовується для змінних, які не слід перепризначати, та прив’язані до контексту, в якому вона вказана.

Ключове слово let дозволяє вам оголошувати змінні, які обмежені областю видимості, виразом чи блоком, у якому вони використовуються. На відміну від const, let не дозволяє змінювати свій контент.

Оголошення let та const мають блокову область видимості, що означає, що вони доступні тільки в межах фігурних дужок { }, що оточують їх.

const та let обмежуються будь-яким блоком — умовна конструкція if, цикл, tre/catch, або просто {}.

На відміну, var обмежена блоком функції, тобто, якщо змінна задекларована десь глибоко у функції, але з ключовим словом var — вона буде доступна з будь-якого місця, але в межах цієї ж функції.

Чи знаєте ви, що до JavaScript ми можемо додати фігурні дужки { }, щоб додати рівень області видимості, де б ми не захотіли?

Так, ми завжди можемо зробити таке:

{

    let block1 = 'First level'; 

    {

        const block2 = 'Be careful!';

        console.log (block1); // показує ‘First level’

        console.log (block2); // показує ‘Be careful!’

    }

    console.log (block1); // показує ‘First level’

    console.log (block2); // Uncaught ReferenceError: block2 is not defined

}

console.log (block1); // Uncaught ReferenceError: block1 is not defined

console.log (block2); // Uncaught ReferenceError: block2 is not defined

Більше прикладів у цьому напрямі:

let counter = 10;

let shouldReset = true;

if (shouldReset) {

let counter = 0; // інша змінна

      console.log (counter); // 0

}

console.log (counter); // Упс… Ви побачите 10

Дві унікальні змінні із різними значеннями.

Це сталося тому, що повторне оголошення counter доступне лише всередині блоку if. А поза блоком if використовується перший counter. Ви бачите, що це дві різні змінні?

// const та let прив’язані до рівня блоку, на якому вони визначені. У даному прикладі var також буде ізольована.

function isolatedFunction () {

  const ISOLATED = 'isolated'; 

  console.log (ISOLATED); // isolated

}

console.log (ISOLATED); // це викличе помилку ReferenceError

let та const прив’язані до рівня блоку, на якому вони визначені

// let не створює властивість глобальному об'єкту, коли її призначають

let global = 'test';

console.log (this.global); // undefined

let та const не створюють властивість глобальному об'єкту, коли їх призначають.

У js є можливість декларувати змінні без ключового слова, вони відразу стають глобальними та доступними по всій програмі. З іншого боку, можна налаштувати так, щоб у  var не було області видимості блоку:

var counter = 10;

var shouldReset = true;

if (shouldReset) {

var counter = 0; 

}

console.log (counter); // Ура! Ви побачите 0

Одна змінна із повторно оголошеним значенням.

Так що таке «Тимчасова мертва»?

Тимчасова мертва зона (ТМЗ) — термін для опису стану, коли змінні недоступні. Вони в області видимості, але не оголошені. Тобто це проміжок часу між створенням зв’язування змінної let або const та її оголошенням (в межах її області).

Змінні let й const існують у ТМЗ з початку своєї області дії доти, доки вони не будуть оголошені.

Ви також можете сказати, що змінні існують у ТМЗ з того місця, де вони пов’язані (коли змінна зв’язується з областю видимості, в якій вона знаходиться) до оголошення (коли ім’я зарезервовано в пам’яті для цієї змінної).

{

  // Це Тимчасова мертва зона для змінної name!

// Це Тимчасова мертва зона для змінної name!

// Це Тимчасова мертва зона для змінної name!

  // Це Тимчасова мертва зона для змінної name!

let name = «John Doe»; // Ура, ми дісталися! ТМЗ більше немає

console.log (name);

}

Ви можете бачити вище, що якби я звертався до змінної name раніше, ніж її оголошення, це видавало б ReferenceError. Через ТМЗ.

Але var цього не зробить. var просто за замовчуванням ініціалізується як undefined, на відміну від іншого оголошення.

Також якщо ви отримаєте доступ до змінної у тимчасовій мертвій зоні через typeof, ви також отримуєте помилку:

{

  // початок Тимчасової мертвої зони для змінної name!

console.log (typeof name); // це викличе помилку ReferenceError: cannot access 'name' before initialization

console.log (typeof aVariableThatDoesntExist); // undefined

  // кінець Тимчасової мертвої зони для змінної name!

let name;

}

Це зроблено так, тому що name не є неоголошеним, він є неініціалізованим. Ви повинні знати про його існування, але не знаєте. Тому попередження здається бажаним й допомагає уникнути помилки у логіці програми.

Приведений вище фрагмент є доказом того, що let явно буде підійматися вище того місця, де воно було об’явлено, оскільки двигун JavaScript попереджає нас про це. Він знає, що ім’я існує (його оголошено), але ми не отримуємо до нього доступ, поки воно не буде ініціалізовано.

Коли змінні підіймаються (hoisting), var за замовчуванням ініціалізується значенням undefined у процесі підняття. let й const також підіймаються, але не встановлюються в undefined, коли вони підіймаються.

І це єдина причина, по якій у нас є «Тимчасова мертва зона». Ось чому це відбувається з let і const, але не з var.

Наступний код демонструє, що «мертва зона» насправді тимчасова (на основі часу), а не просторова (на основі розташування):

    // тут ми знаходимося у ТМЗ та якщо

    // звертатись до `name` буде помилка `ReferenceError`

    let name = «John Doe»; // ТМЗ закінчилась

    printName (); // функція виконується поза межами ТМЗ

}

Але що якщо трохи змінити порядок подій у часі?

if (true) { // нова область видимості, ТМЗ почалась

    printName (); // функція виконується у межі ТМЗ

    let name = «John Doe»; // ТМЗ закінчилась

    function printName () {

        console.log (name); // буде помилка `ReferenceError`

    };

}

Зараз функція printName виконується раніше та намагається отримати доступ до змінної name, і тут виникає помилка ReferenceError, оскільки name знаходиться у ТМЗ, попри те, що оголошення функції розташоване нижче ініціалізації змінної name.

Тимчасова мертва зона також може бути створена для параметрів функцій за замовчуванням. Якщо параметри мають значення за замовчуванням, вони розглядаються як послідовність інструкцій let і підпадають під тимчасові мертві зони:

// Помилка: `x` намагається отримати доступ до `y` в межі ТМЗ

function multiply (x=y, y=0) {

    return x * y;

}

multiply (); // ReferenceError

Видає ReferenceError, тому що при присвоєнні значення змінної x намагаємось отримати доступ до змінної y до того, як її буде проаналізовано механізмом JS. Всі аргументи функції знаходяться всередині ТМЗ, доки вони не будуть проаналізовані.

Висновки

Навіщо ж тоді тимчасові мертві зони? Головна причина полягає в наступному:

  • Це допомагає нам ловити помилки. Дивно мати доступ до змінної до її оголошення. Якщо ви це зробите, зазвичай це випадково, і вас слід попередити про це.
  • Це також дає більш очікувану та раціональну семантику для константи (оскільки константа підіймається, що станеться, якщо програміст спробує використовувати її до того, як вона буде оголошена під час виконання? Яку змінну вона повинна містити в момент, коли вона буде піднята?).

Уникнути проблем, які викликає ТМЗ відносно просто — переконайтеся, що ви визначаєте свої let й const у верхній частині вашої області видимості.