Блочне зв'язування
Так склалось, що одна із найхитріших частин програмування на 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 вводить блочну область видимості, щоб забезпечити більш гнучкий контроль над життєвим циклом змінних.
Блочне оголошення
Блочним називають оголошення, в якому змінні не доступні поза поточною блочною областю видимості. Блочна область видимості виникає:
- всередині функції;
- всередині блоків (між символами
{
та}
).
Багато 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
. Цим ви забезпечуєте початковий рівень імутабельності в коді, який може запобігти виникненню деяких видів помилок.