Что выведет console.log в результате выполнения цикла while?

Дан код:

var i = 10;
var array = [];

while (i--) {
    (function (i) {
        var i = i;
        array.push(function() {
            return i + i;
        });
    })(i);
}    

console.log([
    array[0](),
    array[1](),
])

Что выведет console.log в результате выполнения кода?

Пошаговый разбор выполнения

Цикл while сначала вычисляет условие, и только затем выполняет тело цикла.

В условии используется i-- (постфиксный декремент): в выражение подставляется старое значение, а уменьшение переменной происходит сразу после этого.

Исходный код (для ориентира):

var i = 10;
var array = [];

while (i--) {
    (function (i) {
        var i = i;
        array.push(function() {
            return i + i;
        });
    })(i);
}    

console.log([
    array[0](),
    array[1](),
])

Итерация 1

Старт: i = 10.

Условие while (i--) берёт значение 10 (истина), затем уменьшает i до 9. В тело цикла попадает уже уменьшенное значение внешней i, то есть i === 9, и именно 9 передаётся в IIFE: (function(i){...})(9).

Внутри IIFE создаётся собственная область видимости функции, а параметр i — локальная переменная этой функции.

Строка var i = i; не создаёт «вторую» переменную: в пределах одной функции var не имеет блочной области видимости, а повторное объявление допустимо; фактически остаётся одна переменная i этой функции, которой просто присваивается текущее значение i.

Далее в array кладётся функция, которая возвращает i + i, то есть 9 + 9 = 18.

Итерация 2

Перед проверкой условия: внешняя i = 9.

Условие while (i--) берёт значение 9 (истина), затем уменьшает i до 8. В IIFE передаётся 8, значит сохранённое (замкнутое) значение — 8, и array[1]() вернёт 8 + 8 = 16.

Почему значения не «уплывают»

Ключевой момент: для каждого прохода цикла IIFE вызывается немедленно и получает аргумент (текущее число), а затем внутренняя функция замыкается на переменную i этой конкретной IIFE.

Так как это разные вызовы функции, у них разные окружения (разные «ячейки памяти» для i), поэтому array[0] и array[1] возвращают разные стабильные значения.

Таблица значений по шагам

Номер шагаЗначение, которое проверяет while(i--)Внешняя i после декрементаАргумент IIFE (…)(i)Результат сохранённой функции
1109918
298816
Конструкция var i = i; выглядит как создание копии, но с var внутри одной и той же функции это не отдельная переменная, а повторное объявление той же переменной; такая запись ухудшает читаемость и часто становится источником ошибок при изучении областей видимости.

Мини-схема (что где живёт)

Глобальная область:
  i (внешняя)

Каждая итерация while:
  1) условие использует старое i, затем i уменьшается (i--)
  2) вызывается IIFE с текущим (уже уменьшенным) i
  3) IIFE создаёт свою локальную i
  4) внутренняя функция замыкается на локальную i IIFE

array[0] -> функция с i = 9  -> 9 + 9
array[1] -> функция с i = 8  -> 8 + 8

Теория по задаче

while (condition) и порядок вычисления

У while условие вычисляется перед каждой итерацией, и только при истинности выполняется тело цикла.

Из-за этого выражения с побочными эффектами (например, i--) прямо в условии влияют на то, какое значение окажется в теле цикла.

Постфиксный декремент x--

Постфиксная форма x-- возвращает значение до уменьшения, а уменьшение выполняется сразу после получения значения выражения.

Поэтому в while (i--) проверяется «старое» i, но внутри тела цикла i уже меньше на 1.

Пример по аналогии:

let x = 3;
let y = x--;
console.log(x, y); // x станет 2, y будет 3

var и область видимости функции

Переменные, объявленные через var, имеют область видимости функции (или глобальную, если функция отсутствует), а не область блока { ... }.

Поэтому запись var i внутри IIFE относится к одной и той же локальной переменной этой функции, даже если var i «повторяется» или стоит внутри вложенных блоков.

Также var допускает повторное объявление в той же области видимости без ошибки.

Замыкания и «запоминание» значения

Функция, помещённая в array, использует переменную i, находящуюся во внешней (для неё) функции IIFE, и тем самым образует замыкание.

Так как IIFE вызывается заново на каждой итерации, создаётся новое окружение, и каждая добавленная функция «помнит» своё значение i.

Кратко: краткая выжимка — правильный ответ [18, 16], потому что i-- в условии уменьшает внешнюю i до входа в тело цикла, а IIFE создаёт отдельное замыкание на значение i для каждой итерации.