назад до уроку

Армія функцій

важливість: 5

Наступний код створює масив 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, і рішення стане очевидним.

  1. Функція створює порожній масив shooters:

    let shooters = [];
  2. Наповнює його функціями у циклі через 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); }
    ];
  3. Функція повертає масив.

    Потім, виклик будь-якого елемента масиву, наприклад 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, та інші сценарії, де такі проблеми є реальними.

Відкрити рішення із тестами в пісочниці.