Прежде чем начать чтение

Чтобы хорошо понять эту тему, вам необходимо сначала ознакомиться с концепциями области действия блоков, функций, замыканий и асинхронного JS.

Введение: Тайна

Если вы попытаетесь запустить этот JS-код, вы получите результат, которого, возможно, не ожидали.

for (var i = 0; i < 5; i++) {
   setTimeout(() => {
     console.log(i)
   })
}

Приведенный выше фрагмент кода выведет:
5
5
5
5
5

Поражен? Да, это расстраивает.

Прежде чем мы углубимся в реальную проблему

Да, я знаю, что вам не терпится и вы хотите погрузиться в основную тему, но давайте перед этим обсудим некоторые вещи, и я обещаю, что вы поймете проблему лучше и глубже.

Как вы уже знаете (если нет, сначала изучите это), переменные, которые инициализируются с помощью «var», не относятся к области действия блока, проще говоря, они не учитывают область действия блока. Давайте теперь посмотрим на этот пример:

if(1===1) {
 var a = 'Hello'; 
 
 console.log('1===1: ', a);
 
 setTimeout(() => {
   console.log('from set time out: ', a);
 })
}

if(2===2) {
 var a = 'Bye'; 
 
 console.log('2===2: ', a);
}

if(3===3) {
 console.log('3===3: ', a);
}

После запуска этого кода будет напечатано следующее:

1===1: Привет
2===2: Пока
3===3: Пока
после установленного времени ожидания: Пока

Так что же там происходит? Хорошо, сначала мы инициализировали переменную 'a' как 'Hello', позже в другом блоке мы говорим ''var a = 'bye' '' НО, поскольку переменные 'var' не являются в области блока мы не создавали новую переменную, а просто переназначили существующую переменную и присвоили ей новое значение: 'пока'

Итак, во втором и третьем блоках if мы печатаем переназначенные значения. setTimeout() также печатает переназначенное значение, поскольку оно выполняется после того, как стек вызовов пуст (сначала изучите асинхронный JS, чтобы понять это), после завершения стека вызовов с кодом синхронизации отдыха только тогда выполняется setTimeout() и он запомнил ссылку на переменная «a», которая была переназначена, поэтому печатает ее текущее значение. Теперь давайте заменим «var» на «let»:

if(1===1) {
 let a = 'Hello'; 
 
 console.log('1===1: ', a);
 
 setTimeout(() => {
   console.log('from set time out: ', a);
 })
}

if(2===2) {
 let a = 'Bye'; 
 
 console.log('2===2: ', a);
}

if(3===3) {
 console.log('3===3: ', a);
}

Теперь мы получаем такой результат:

1===1: Привет
2===2: Пока
ReferenceError: a не определен
console.log('3===3: ', a);

Это произошло потому, что переменные 'let' имеют область действия блока, поэтому каждый раз, когда мы говорим '' let a = 'someValue' '' внутри разных блоков, создаются разные переменные, мы не переназначаем существующую переменную, которая была изначально создается, как в примере с 'var', но мы создаем новые переменные. В третьем блоке if мы столкнулись с ошибкой, потому что в этом блоке if или в глобальной области видимости у нас не было доступной переменной 'a'.

Декодировать setTimeout() внутри цикла var for

for (var i = 0; i < 5; i++) {
   setTimeout(() => {
     console.log(i)
   })
}

Итак, на серверной части движка происходит примерно следующее:

  1. Для каждой итерации создается новый блок.
  2. Происходит повторное создание (на самом деле переназначение): var i = 0, var i = 1, var i = 2 и т. д., поскольку «var» не имеет области действия блока, мы просто переназначаем одну и ту же переменную;
  3. условие ‘i ‹ 5’ проверяется
  4. Если условие передано, логика выполняется внутри блока for, в этом случае создается замыкание, поскольку setTimeout() — это функция, и внутри нее есть другая функция, которая ссылается на внешнюю переменную «i», поэтому setTimeout фактически не выполняется напрямую, он отправляется в WEB API, и функция внутри него запоминает ссылку на переменную 'i' (не значение, а ссылку), и мы переходим к следующему шагу 5, который описан ниже.
  5. я увеличивается
  6. Все предыдущие шаги повторяются до остановки цикла.
  7. Когда цикл завершается, логика внутри всех setTimeout() выполняется в правильном порядке (в зависимости от их порядка в очереди обратного вызова). Итак, теперь всефункции внутри setTimeout() видят ссылку на одну и ту же переменную, и значение этой переменной равно 5.

Декодирование setTimeout() внутри цикла let for

for (let i = 0; i < 5; i++) {
   setTimeout(() => {
     console.log(i)
   })
}

Итак, на серверной части движка происходит примерно следующее:

  1. Для каждой итерации создается новый блок.
  2. Инициализация новой переменной «i» на итерацию, на каждый блок. Поскольку «let» имеет блочную область действия, он не просто переназначает значение переменной, но фактически создает новую копию переменной «i» с новым значением.
  3. i увеличивается (если это не самая первая итерация)
  4. условие ‘i ‹ 5’ проверяется
  5. Если условие передано, логика выполняется внутри блока for, в этом случае создается замыкание, поскольку setTimeout() — это функция, и внутри нее есть другая функция, которая ссылается на внешнюю переменную «i», поэтому setTimeout фактически не выполняется напрямую, он отправляется в WEB API, и функция внутри него запоминает ссылку на переменную 'i' (не значение, а ссылку), и мы переходим к следующему шагу 5, который описан ниже.
  6. Все предыдущие шаги повторяются до остановки цикла.
  7. Когда цикл завершается, логика внутри всех setTimeout() выполняется в правильном порядке (в зависимости от их порядка в очереди обратного вызова). Итак, теперь все функции внутри setTimeout() видят ссылки на разные соответствующие переменные i в зависимости от того, в каком блоке они находились, а с помощью замыкания они имеют ссылку на эти переменные. Итак, еще раз, каждый setTimeout() теперь ссылается на разные переменные, поскольку новые переменные создавались на каждой итерации, в каждом новом блоке.

Другой способ исправить setTimeout() внутри цикла var

Не забывайте, что «var» относится не к блоку, а к функции. Итак, если мы запустим код ниже:

for (var i = 0; i < 5; i++) {
 (
   function(index) {
     setTimeout(() => {
       console.log(index)
     }, 1000)
    }
 )(i)
}

Итак, в приведенном выше фрагменте кода мы передаем «i» в IIFE (изучите IIFE, чтобы лучше понять). Мы передаем его только потому, что для ссылки теперь каждый раз, когда создается новый IIFE с новой копией переменной, «var» не ограничивается блоками, но ограничивается функциями. Нет необходимости передавать «i» в функцию, мы также можем сделать следующее, чтобы создавать новую переменную на каждой итерации внутри IIFE:

for (var i = 0; i < 5; i++) {
 (
   function() {
   //of course ‘let’ would also work
     var index = i;
     setTimeout(() => {
       console.log(index)
     }, 1000)
   }
 )()
}

Таким образом, оба приведенных выше фрагмента кода печатают:
0
1
2
3
4

Возможно, Вы что-то пропустили…

Это уже упоминалось в разделах Декодирование setTimeout() внутри цикла let for и Декодирование setTimeout() внутри цикла let for, но если вы это пропустили, я упомяну, что в циклах var увеличение 'i' происходит в конце каждой итерации, тогда как в циклах let это происходит в начале каждой итерации (после инициализации новой переменной 'i'). Вот почему этот фрагмент кода:

for (var i = 0; i < 5; i++) {
   setTimeout(() => {
     console.log(i)
   }, 1000)
}

Печатает 5, а не 4. Также мы можем доказать это с помощью этого кода:

for (var i = 0; i < 5; i++) {
   console.log(i)
   setTimeout(() => {
     console.log(i)
   }, 1000)
}

console.log() вне setTimeout() печатает 0, 1, 2, 3, 4, но console.log() внутри setTimeout() печатает 5 секунд, как упоминалось ранее.