Блочне зв'язування

Так склалось, що одна із найхитріших частин програмування на JavaScript — спосіб, яким оголошуються змінні. У більшості С-подібних мов змінні (або їх зв'язування) створюються там, де їх оголошують. Проте в JavaScript це не зовсім так. Місце створення ваших змінних залежить від того, як ви їх оголосили і ECMAScript 6 надає вам простіший спосіб контролювати область видимості. Ця глава ілюструє чому класичне var оголошення може заплутати, вводить блочне зв'язування в ECMAScript 6, а тоді пропонує деякі рекомендації для їх використання.

var-оголошення та виринання

Використовуючи var оголошення, змінна переміщується на початок функції (або в глобальну область видимості, якщо оголошення відбулось поза функцією) незалежно від того, де воно відбулось насправді — це називається виринання. Щоб продемонструвати цю поведінку, розглянемо наступну функцію:

function getValue(condition) {

    if (condition) {
        var value = "blue";

        // інший код

        return value;
    } else {

        // тут змінна value буде існувати зі значенням undefined

        return null;
    }

    // тут змінна value буде існувати зі значенням undefined
}

Якщо ви не знайомі з JavaScript, скоріш за все ви очікуєте, що змінна value буде створена тільки тоді, коли condition буде дорівнювати true. Насправді value буде створена незалежно від цього. Під капотом рушій JavaScript змінює функцію getValue таким чином, що вона виглядає так:

function getValue(condition) {

    var value;

    if (condition) {
        value = "blue";

        // інший код тут

        return value;
    } else {

        return null;
    }
}

Змінна value виринає вгорі блоку і в тому ж місці відбувається ініціалізація. Це означає, що змінна value все ще доступна в блоці else. Проте, якщо звернутись до неї, отримаємо undefined, тому що вона ще не проініціалізована.

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

Блочне оголошення

Блочним називають оголошення, в якому змінні не доступні поза поточною блочною областю видимості. Блочна область видимості виникає:

  1. всередині функції;
  2. всередині блоків (між символами { та } ).

Багато C-подібних мов програмування працюють за принципом блочної області видимості, тому введення її в ECMAScript 6 надає таку ж гнучкість (і однорідність) в JavaScript.

Let-оголошення

Синтаксис let оголошення нічим не відрізняється від var. Ви можете просто замінити var на let, проте тим самим ви обмежите область видимості змінної блоком, в якому вона оголошена (крім того є ще декілька важливих відмінностей, про які ми поговоримо згодом). Так як з let оголошенням змінна не буде доступна поза блоком, в якому вона оголошена, вам буде потрібно здійснювати let оголошення на початку блоку, щоб вона була доступна також у внутрішніх блоках. Наприклад:

function getValue(condition) {

    if (condition) {
        let value = "blue";

        // інший код

        return value;
    } else {

        // змінна value не буде існувати в цьому блоці

        return null;
    }

    // змінна value не буде існувати в цьому блоці
}

Поведінка цієї версії функції getValue більше схожа на те, що ви очікуєте від інших C-подібних мов програмування. Оскільки змінна value оголошена, використовуючи let, а не var, оголошення не виринає на початку визначення функції, і змінна value буде знищена після того, як виконається блок if. Якщо ж condition буде false, value ніколи не буде оголошена або проініціалізована.

Жодного перевизначення

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

var count = 30;

// Помилка синтаксису
let count = 40;

В цьому прикладі count визначена двічі: одного разу із var, іншого — використовуючи let. Оскільки let не перевизначає ідентифікатори, які вже визначені в даній області видимості, оголошення викликає помилку. Натомість ніякої помилки не буде, якщо let оголошення створить нову змінну в області видимості з таким самим ім’ям, як і в змінної, яка знаходиться в області видимості, яке міститься в попередньому, що й ілюструє наступний код:

var count = 30;

// Не викличе помилку
if (condition) {

    let count = 40;

    // ще якийсь код
}

Це let оголошення не викличе помилку, тому що воно створить нову змінну count лише у if виразі, а не в навколишньому блоці. В середині if блоку нова змінна перекриває глобальну count, запобігаючи доступу до неї, допоки виконання не покине блок.

const-оголошення

Інший спосіб визначення змінних в ECMAScript 6 — використовувати синтаксис оголошення const. Змінні, оголошені використовуючи const, вважаються константами, тобто вони не можуть бути змінені присвоєнням нового значення. Тому кожна const змінна повинна бути проініціалізована і оголошена так, як це показано в прикладі:

// Правильна константа
const maxItems = 30;

// Помилка синтаксису: відсутня ініціалізація
const name;

Змінна maxItems проініціалізована, тому const оголошення має працювати без проблем. Натомість змінна name спричинить помилку синтаксису, якщо ви спробуєте запустити програму, яка містить цей код, тому що вона не проініціалізована.

const-оголошення проти let-оголошення

Оголошення константи, так як і let оголошення є блочними оголошеннями. Це означає, що константи будуть знищенні відразу ж, як виконання покине блок, в якому вони оголошені, також не буде виринання, що і показано в прикладі:

if (condition) {
    const maxItems = 5;

    // ще якийсь код
}

// maxItems тут недоступна

В цьому коді константа maxItems оголошена в if виразі. Коли виконання виразу закінчиться, maxItems буде видалена і не буде доступна поза блоком.

Інша подібність let та const — оголошення викличе помилку, якщо використати для оголошення ідентифікатор змінної, яка вже оголошена в цій області видимості. І не важливо чи змінна була оголошена використовуючи var (для глобальної або для області видимості на рівні функції) чи let (для області видимості на рівні блоку). Для прикладу, розглянемо цей код:

var message = "Hello!";
let age = 25;

// Оголошення кожної із цих змінних викличе помилку
const message = "Goodbye!";
const age = 30;

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

Незважаючи на ці подібності, є одна велика відмінність між let та const, яку потрібно пам'ятати. Спроба призначити const-змінну іншій вже визначеній константі викличе помилку як в strict, так і в non-strict режимі.

const maxItems = 5;

maxItems = 6;      // викличе помилку

Так як і константи в інших мовах, змінній maxItems не можна призначити нове значення пізніше. Проте, на відміну від констант в інших мовах, значення може бути видозмінене, якщо це об'єкт.

Оголошення об'єктів з const

Оголошення з const запобігає видозміні зв'язку, а не самого значення. Це означає, що const визначення об'єктів не запобігає їх модифікації. Наприклад:

const person = {
    name: "Nicholas"
};

// працює
person.name = "Greg";

// викличе помилку
person = {
    name: "Greg"
};

Тут посилання person створюється з початковим значенням об'єкта із однією властивістю. Зміна person.name не викличе помилку, тому що змінюється значення person, а не те, на що person посилається. Коли цей код спробує призначити нове значення (намагаючись змінити посилання), буде викликана помилка. Дуже просто заплутатись в тонкощах того, як const працює із об'єктами. Просто запам'ятайте: const запобігає видозміні посилання, а не його значення.

W> В деяких браузерах реалізована попередня перед ECMAScript 6 версія const, тому зважайте на це, використовуючи цей вид визначення. Реалізації варіюються від простого синоніму var (дозволяючи перевизначення значення), до визначення констант насправді, але тільки в глобальній та області визначення на рівні функції. Тому будьте особливо обережні, використовуючи const в продакшині. Функціональності, яку ви очікуєте, може не бути.

Тимчасова мертва зона

На відміну від var, let та const не мають виринаючої характеристики. До змінних оголошених раніше, не можна звернутись доти, доки вони не визначені знову. Спроба зробити це спричинить помилку посилання, навіть якщо використовувати таку звичну та безпечну операцію як typeof:

if (condition) {
    console.log(typeof value);  // Помилка посилання!
    let value = "blue";
}

Тут змінна value визначена та ініціалізована, використовуючи let, проте вираз ніколи не буде виконаний, оскільки попередній рядок призведе до помилки. Проблема в тому, що value існує в області, відомій JavaScript спільноті як тимчасова мертва зона (ТМЗ). ТМЗ ніколи не згадується безпосередньо в специфікації, проте це термін, який часто вживається, щоб описати неспливаючу поведінку let та const. Цей розділ розповідає про деякі тонкощі визначення в місцях, які є ТМЗ, і хоча всі приклади використовують let, те саме рівноцінно також для const.

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

Це справджується кожного разу, коли ви намагаєтесь використати змінну визначену з let перш ніж вона була визначена, навіть використовуючи звичний і безпечний оператор typeof. Проте ви можете використовувати typeof зі змінними поза блоком, в якому вони визначені, хоч це може не дати результату. Розглянемо цей код:

console.log(typeof value);     // "undefined"

if (condition) {
    let value = "blue";
}

Змінної value немає в ТМЗ, коли виконується оператор typeof, тому що це відбувається поза блоком в якому value визначена. Це означає, що ще немає посилання на value, і typeof просто повертає "undefined".

ТМЗ — всього лише один унікальний аспект блочного зв'язування. Іншим аспектом є використання в середині циклів.

Блочне зв'язування в середині циклів

Можливо, випадок, коли розробникам найбільше потрібна блочна область видимості для змінних — цикли for, де оголошена змінна лічильника має використовуватись тільки в середині циклу. Наприклад, цей код — цілком звичний в JavaScript:

for (var i=0; i < 10; i++) {
    process(items[i]);
}

// змінна i досі доступна
console.log(i);                     // 10

В інших мовах, де блочна область видимості за замовчуванням, код цього типу працює як і планувалось, і тільки цикл for має доступ до змінної i. В JavaScript змінна i досі доступна після того, як цикл завершиться, через виринаючий характер var. Натомість використання let як в коді наведеному нижче, дозволить вам отримати очікувану поведінку:

for (let i=0; i < 10; i++) {
    process(items[i]);
}

// змінна i тут недоступна, тому буде викликана помилка
console.log(i);

У цьому прикладі змінна i тільки існує всередині циклу for. Як тільки цикл закінчить своє виконання, змінна буде знищена та стане недоступною деінде.

Функції в циклах

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

var funcs = [];

for (var i=0; i < 10; i++) {
    funcs.push(function() { console.log(i); });
}

funcs.forEach(function(func) {
    func();     // виводить число "10" десять разів
});

Ви, мабуть, очікуєте, що цей код виведе числа від 0 до 9, проте він виводить число 10 десять разів підряд. Все тому, що змінна i спільна для кожної ітерацій циклу, тобто функції, які створюються всередині циклу, беруть посилання на ту ж змінну. Коли цикл закінчиться, значення змінної i буде рівне 10, тому коли виконується console.log(i), це і є значенням, яке виводиться кожного разу.

Щоб вирішити цю проблему, розробники використовують негайно виконуваний функціональний вираз (НВФВ) (immediately-invoked function expression (IIFE)) всередині циклів, щоб вимушено створити нову копію змінної, яку вони хочуть використовувати в ітерації, тобто як в коді нижче:

var funcs = [];

for (var i=0; i < 10; i++) {
    funcs.push((function(value) {
        return function() {
            console.log(value);
        }
    }(i)));
}

funcs.forEach(function(func) {
    func();     // виводить 0, тоді 1, тоді 2, і так до 9
});

Тут, в середині циклу, використовується НВФВ. Змінна i передається до НВФВ, який створює власну копію і зберігає її як value. Це значення змінної, яке використовується функцією для цієї ітерації, тому викликаючи кожну функцію, ми отримуємо значення лічильника циклу — від 0 до 9. На щастя, блочна область видимості з let та const можуть спростити цей цикл для вас.

let-оголошення в циклах

Тип оголошення з let спрощує цикли, фактично виконуючи те, що НВФВ робили в попередньому прикладі. Кожної ітерації цикл створює нову змінну та ініціалізує її зі значенням змінної з таким самим ім'ям в попередній ітерації. Це означає, що ви можете не використовувати НВФВ і отримати очікуваний результат, як цей:

var funcs = [];

for (let i=0; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
    });
}

funcs.forEach(function(func) {
    func();     // виводить 0, тоді 1, тоді 2, і так до 9
})

Цей код працює так само, як код, який використовує var та НВФВ, проте, можливо, він більш зрозумілий. Оголошення із let створює нову змінну кожного разу впродовж циклу, тому кожна функція створена в циклі отримує власну копію i. Кожна копія i має значення, яке було призначене на початку ітерації циклу, в якому вона була створена. Теж саме підійде також для for-in та for-of циклів, як це показано тут:

var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };

for (let key in object) {
    funcs.push(function() {
        console.log(key);
    });
}

funcs.forEach(function(func) {
    func();     // виводить "a", тоді "b" і тоді "c"
});

В цьому прикладі цикл for-in відтворює таку ж поведінку, як і в цикл for. Кожного разу впродовж циклу створюється нове посилання key, тому кожна функція має власну копію змінної key. Як результат кожна функцію виводить інше значення. Якщо б ми використовували var для визначення key, кожна функція виводила б "c".

|> Важливо розуміти, що поведінка let визначення в циклах спеціально визначена в специфікації, і не пов'язана з невиринаючою властивістю let. Насправді ж ранні реалізації let не мали цієї поведінки — це було додано пізніше в процесі створення специфікації.

Оголошення констант в циклах

Фактично специфікація ECMAScript 6 не забороняє const визначення в циклах; проте поведінка залежить від циклу, який ви використовуєте. Для звичного циклу for ви можете використовувати const в ініціалізації, але цикл виведе попередження, якщо ви спробуєте змінити значення. Наприклад:

var funcs = [];

// виведе помилку після першої ітерації

for (const i=0; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
    });
}

В цьому коді змінна i визначена як константа. Перша ітерація циклу, де i рівне 0, виконається успішно. Помилку викличе виконання i++, так як це спроба змінити константу. Тому ви можете використовувати const для того, щоб визначати змінні в ініціалізації циклу тільки якщо ви не будете їх змінювати.

З іншого боку, використовуючи const змінну в циклах for-in та for-of, поведінка буде така ж, як і let змінної. Тому наступний код помилки не викличе:

var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };

// не виведе помилку
for (const key in object) {
    funcs.push(function() {
        console.log(key);
    });
}

funcs.forEach(function(func) {
    func();     // виведе "a", тоді "b" і тоді "c"
});

Цей код практично такий самий, як і в другому прикладі в розділу «let оголошення в циклах». Єдиною відмінністю є те, що значення key неможливо змінити всередині циклу. Цикли for-in та for-of працюють із const, тому що ініціалізатор циклу створює нове посилання при кожній ітерації, а не намагається змінити значення за посиланням, яке вже існує (як це відбувалось в попередньому прикладі, використовуючи for, а не for-in).

Глобальне блочне зв'язування

Іншою відмінністю let та const від var є поведінка в глобальній області видимості. Коли створюють нову глобальну змінну використовуючи var, створюється глобальна змінна, яка є полем глобального об'єкту (windows браузерах). Це означає, що ви можете випадково перезаписати існуючу глобальну змінну, використовуючи var, так як відбувається тут:

// в браузері
var RegExp = "Hello!";
console.log(window.RegExp);     // "Hello!"

var ncz = "Hi!";
console.log(window.ncz);        // "Hi!"

Хоча глобальний RegExp визначений в window, його можна перезаписати, використовуючи var оголошення. Цей приклад оголошує нову глобальну змінну RegExp, яка перезаписує оригінал. Так само як і ncz визначений як глобальна змінна і відразу ж визначається як поле об'єкта windows. Так JavaScript працював завжди.

Проте якщо ви будете використовувати let та const в глобальній області видимості, будуть створені нові глобальні посилання на змінні без додавання їх як поля глобального об'єкту. Це також означає, що ви не зможете перезаписати глобальну змінну, використовуючи let та const, а тільки перекрити їх. Наприклад:

// в браузері
let RegExp = "Hello!";
console.log(RegExp);                    // "Hello!"
console.log(window.RegExp === RegExp);  // false

const ncz = "Hi!";
console.log(ncz);                       // "Hi!"
console.log("ncz" in window);           // false

В цьому коді нове let оголошення для RegExp створює нове посилання, яке перекриває глобальну RegExp. Тобто windows.RegExp та RegExp — не одне і те ж, тому немає ніяких збоїв у глобальній області видимості. Також const оголошення nzt створює нове посилання, але не створює нове поле в глобальному об'єкті. Ця особливість робить let та const набагато безпечнішими для використання в глобальній області видимості, якщо ви не хочете створювати поле в глобальному об'єкті.

|> Ви як і раніше можете використовувати var для оголошення змінних в глобальній області видимості, якщо ваш код має бути доступний через глобальний об'єкт. Це може бути необхідним в браузерах, коли вам потрібний доступ до коду із іншого вікна чи фрейму.

Рекомендаці щодо блочного зв'язування

Допоки ECMAScript 6 був у розробці, була розповсюджена думка про те, що для оголошення змінних потрібно використовувати let за замовчуванням, а не var для оголошення змінних. Для багатьох JavaScript розробників поведінка let є саме такою, якою мала би бути поведінка var, тому пряме заміщення є логічним. У цьому випадку ви використовуватимете const тільки для змінних, які не повинні змінюватись.

Однак, що більше розробників мігрувало на ECMAScript 6, то більшу популярність набрав альтернативний підхід: використовувати const за замовчуванням, а let тільки тоді, коли ви знаєте, що значення змінної потрібно буде змінити. Основною причиною для цього є те, що змінна не повинна змінюватись після ініціалізації, тому що несподівані зміни значення є джерелом помилок. Цей підхід набрав популярність, проте варто вивчити ваш код, щоб зрозуміти як пишите код на ECMAScript 6 ви.

Підсумок

Блочне зв'язування, з використанням let та const, принесло лексичну область видимості в JavaScript. Ці оголошення не виринають та існують тільки всередині блоку, в якому вони були визначені. Крім того, блочне зв'язування ECMAScript 2015 надає поведінку, яка є більш схожою на поведінку змінних в інших мовах, тим самим оберігає від випадкових помилок, тому що змінні можуть бути визначенні саме там, де вони необхідні. Побічним ефектом є те, що ви не можете звернутись до змінної, перш ніж вона була оголошена, навіть із безпечними операторами, такими як typeof. Спроба це зробити спровокує помилку у зв'язку із необхідністю наявності зв'язаного посилання в тимчасовій мертвій зоні (ТМЗ).

В більшості випадків поведінка lеt та const схожа до поведінки var, однак в циклах це не так. Для let та const цикли for-in та for-of створюють нове посилання для кожної ітерації впродовж циклу. Це означає, що функції створенні в тілі циклу можуть звернутись до значення, яке зв'язане з ним впродовж поточної ітерації, а не навіть після закінчення останньої ітерації (поведінка var). Це ж вірно і для let оголошень у циклах for, в той час як спроба використання const визначення для циклу for спричинить помилку.

Поточною рекомендацією для блочного зв'язування є використання const в усіх випадках, окрім тих, коли ви знаєте, що повинні змінити значення змінної, тоді використовуйте let. Цим ви забезпечуєте початковий рівень імутабельності в коді, який може запобігти виникненню деяких видів помилок.

results matching ""

    No results matching ""