Армія функцій
Наступний код створює масив shooters
.
Кожна функція має вивести свій номер. Але щось не так…
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // створюємо функцію стрільця,
alert( i ); // що має показувати свій номер
};
shooters.push(shooter); // додаємо її до масиву
i++;
}
// ...і повертаємо масив стрільців
return shooters;
}
let army = makeArmy();
// всі стрільці показують 10 замість своїх номерів 0, 1, 2, 3...
army[0](); // 10 від стрільця за номером 0
army[1](); // 10 від стрільця за номером 1
army[2](); // 10 ...і так далі.
Чому всі функції показують однакове значення?
Виправте код так, щоб він працював як передбачалося.
Давайте розберемося, що саме відбувається всередині функції makeArmy
, і рішення стане очевидним.
-
Функція створює порожній масив
shooters
:let shooters = [];
-
Наповнює його функціями у циклі через
shooters.push(function)
.Кожен елемент є функцією, тому отриманий масив виглядає так:
shooters = [ function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); } ];
-
Функція повертає масив.
Потім, виклик будь-якого елемента масиву, наприклад
army[5]()
отримає елементarmy[5]
з масиву (який є функцією) і викликає її.Чому всі функції показують однакове значення,
10
?Зверніть увагу, що всередині функцій
shooter
немає локальної змінноїi
. Коли така функція викликається, вона приймаєi
зі свого зовнішнього лексичного середовища.Тоді яке буде значення
i
?Якщо ми подивимося на код:
function makeArmy() { ... let i = 0; while (i < 10) { let shooter = function() { // функція shooter alert( i ); // має показати свій номер }; shooters.push(shooter); // додати функцію до масиву i++; } ... }
Ми бачимо що усі функції
shooter
створені в лексичному середовищі функціїmakeArmy()
. Але коли ми викликаємоarmy[5]()
, функціяmakeArmy
вже закінчила свою роботу, і остаточне значенняi
це10
(циклwhile
зупиняється наi=10
).В результаті всі функції
shooter
отримують однакове значення із зовнішнього лексичного середовища, тобто останнє значення,i=10
.Як ви можете бачити вище, на кожній ітерації циклу
while {...}
, створюється нове лексичне середовище. Отже, щоб виправити це, ми можемо скопіювати значенняi
у змінну всередині блокуwhile {...}
, ось так:function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; let shooter = function() { // функція shooter alert( j ); // має показати свій номер }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); // Тепер код працює правильно army[0](); // 0 army[5](); // 5
Тут
let j = i
оголошує локальну зміннуj
та копіює до неї номер ітерації зі змінноїi
. Примітиви копіюються “за значенням”, тому ми фактично отримуємо незалежну копіюi
, що належить до поточної ітерації циклу.Функції тепер працюють правильно, тому що змінна
i
“живе” трохи ближче. Не в лексичному середовищі викликуmakeArmy()
, але в лексичному середовищі, яке відповідає поточній ітерації циклу:Такої проблеми також можна було б уникнути, якби ми використали цикл
for
з самого початку, ось так:function makeArmy() { let shooters = []; for(let i = 0; i < 10; i++) { let shooter = function() { // функція shooter alert( i ); // має показати свій номер }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5
Це, по суті, те саме, тому що
for
на кожній ітерації створює нове лексичне середовище зі своєю змінноюi
. Томуshooter
згенерований на кожній ітерації бере посилання на зміннуi
, з тієї самої ітерації.
Тепер, коли ви доклали так багато зусиль, щоб прочитати це, остаточний рецепт такий простий – використовуйте цикл for
, ви можете задатися питанням – чи було воно того варте?
Ну, якби ви могли легко відповісти на запитання, ви б не читали рішення. Тож, сподіваюся, це завдання допомогло вам трохи краще зрозуміти як все працює.
Крім того, на практиці бувають випадки, коли віддають перевагу while
замість for
, та інші сценарії, де такі проблеми є реальними.