Замыкание и i-- в while: что выведет console.log

Дан код:

let i = 10, arr = [];
while (i--) {
    arr.push(() => i + i);  
}
console.log([arr[0](), arr[0]()]); 

Что выведет console.log?

Разбор кода по шагам

Исходный код:

let i = 10, arr = [];
while (i--) {
    arr.push(() => i + i);  
}
console.log([arr[0](), arr[0]()]);
Постфиксная форма декремента x-- возвращает старое значение выражения, а уменьшение переменной выполняется сразу после вычисления этого выражения.

Что делает условие while

Условие while (i--) проверяет “истинность” по значению i до уменьшения, а само уменьшение i выполняется после вычисления условия и до выполнения тела цикла.

Трассировка (ключевые моменты):

МоментЗначение i перед проверкойРезультат выражения i--Входит в цикл?Значение i внутри тела
Старт1010да9
...............
Последний заход11да0
Проверка после цикла00нет(тело не выполняется)

Важно: когда i становится 0, выражение i-- возвращает 0 (это “ложь”), но затем всё равно уменьшает i до -1, даже несмотря на то, что тело цикла уже не выполняется.

Итог после завершения while: i равно -1.

Что именно кладётся в arr

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

Ключевая идея: замыкание в JavaScript — это не “снимок” значения переменной, а доступ к самой переменной через лексическое окружение; если переменная меняется, то при вызове функции будет использовано актуальное на момент вызова значение.

Схема (упрощённо):

[внешнее окружение]
  i  ---> (одна и та же переменная, меняется в while)
  arr -> [ f0, f1, f2, ... ]
           |
           +--> f0: () => i + i  (читает i при вызове)
Наличие let само по себе не гарантирует “новую i на каждой итерации” в любом цикле: если переменная объявлена один раз снаружи, а затем изменяется, то все замыкания будут ссылаться на одну и ту же переменную.

Почему результат именно [-2, -2]

  1. После окончания цикла i равно -1 (это следует из трассировки условия while (i--)).
  2. arr[0] — это функция () => i + i, которая при вызове читает текущее значение i из внешней области видимости.
  3. Поэтому при каждом вызове arr[0]() вычисляется -1 + -1, то есть -2.

Следовательно, console.log([arr[0](), arr[0]()]); выводит [-2, -2].

Как получить другие результаты (для понимания)

Вариант A: “зафиксировать” значение в отдельной переменной

Идея: внутри тела цикла создать новую константу, равную текущему i, и замыкать уже её (то есть “снимок” значения).

let i = 10, arr = [];
while (i--) {
    const snapshot = i;
    arr.push(() => snapshot + snapshot);
}
console.log([arr[0](), arr[0]()]);

В этом случае первая функция будет возвращать 18, потому что на первой итерации внутри тела i уже стало 9, значит snapshot равно 9, и 9 + 9 = 18.

Вариант B: Отличие от `for (let i = ...)`

При создании замыканий в циклах типичная ошибка связана с тем, что несколько функций могут разделять одно и то же лексическое окружение с изменяемой переменной, из‑за чего при вызове они видят “последнее” значение.

В конструкции for (let i = ...) часто ожидается другое поведение, потому что переменная i связана с механизмом итераций цикла for; это нередко приводит к тому, что замыкания получают значения “по итерациям”, а не одно общее изменяемое значение. Для while с внешней переменной, изменяемой декрементом, такого эффекта по умолчанию нет.

Кратко: в while (i--) условие использует старое значение i, затем уменьшает i, после завершения цикла i становится -1; все функции в arr читают одну и ту же переменную i при вызове, поэтому arr[0]() возвращает -2, а итоговый вывод — [-2, -2].