Перейти к содержимому
Теория · Замыкания и this · урок 4 из 11

Замыкания в цикле

В прошлом уроке к каждой карточке-функции была прикреплена записка — переменная. Записка не копия: функция держит саму переменную. Поэтому функция видит то значение, которое лежит в переменной в момент вызова функции:

JavaScript
let n = 1;
const show = () => n;

n = 2;
console.log(show()); // 2

Давайте разберём по строкам:

  1. let n = 1 создаёт переменную со значением 1.
  2. const show = () => n создаёт функцию, которая читает n. Значение 1 она себе не копирует.
  3. n = 2 меняет ту самую переменную.
  4. show() читает n в момент вызова и возвращает 2.

Это важно, когда функции создают в цикле, а вызывают после него. Тогда всё решает вопрос: сколько записок было в цикле — одна на всех или своя на каждом круге?

let в заголовке for

Цикл ниже складывает в массив три стрелочные функции. Их вызывают уже после цикла, и каждая возвращает своё число:

JavaScript
const fns = [];
for (let i = 0; i < 3; i++) {
    fns.push(() => i * 10);
}

console.log(fns[0](), fns[1](), fns[2]()); // 0 10 20

Переменная, объявленная через let в заголовке for, создаётся заново на каждой итерации цикла. Каждый круг идёт так:

  1. Создаётся новая i и получает значение прошлой.
  2. Со второго круга к новой i сначала применяется i++, а потом условие i < 3 проверяет её.
  3. Стрелочная функция, созданная на этом круге, замыкает эту i.

Эту i больше никто не меняет: следующий круг работает уже со своей переменной. Каждая карточка уносит свою записку. Поэтому первая функция возвращает 0, вторая 10, третья 20.

Здесь аналогия неточна: новая записка не пустая. На неё сразу переписывают значение с записки прошлого круга.

Цикл for…of с const устроен так же: у каждой итерации своя переменная.

Одна var на цикл

Слово var создаёт переменную с областью видимости функции и не замечает блоков. Такая i одна на весь цикл, а не своя на каждую итерацию. Все функции, созданные в цикле, замыкают одну и ту же переменную: одна записка прикреплена сразу ко всем карточкам.

Значение они читают при вызове, когда цикл уже закончился. Если вызвать функцию сразу, на той же итерации, разницы между let и var не видно.

После цикла в i лежит значение, на котором условие стало ложным. В цикле for (var i = 0; i < 3; i++) это 3, а не 2. Последние шаги цикла идут так:

  1. Последняя итерация работает с i, равной 2.
  2. i++ увеличивает i до 3.
  3. Проверка i < 3 даёт false и останавливает цикл.

Этот вопрос часто задают на собеседовании: функции создают в цикле с var и спрашивают, что они вернут после цикла. Ответ: все вернут одно и то же, последнее значение i, а с let каждая вернёт своё. Хороший ответ называет и причину: var даёт одну переменную на весь цикл, а функции читают её уже после цикла.

Частая ошибка

В цикле с функциями написали var вместо let:

JavaScript
const fns = [];
for (var i = 0; i < 3; i++) { // ✗
    fns.push(() => i * 10);
}

console.log(fns[0](), fns[1](), fns[2]()); // 30 30 30

Ошибки нет, но все три функции возвращают 30. Они читают одну общую i, а при вызове в ней уже 3. Замените var на let: тогда каждая итерация получит свою i, и программа напечатает 0 10 20.

Главное

  • Замыкание читает значение переменной в момент вызова, а не в момент создания функции.
  • let в заголовке for создаёт новую переменную на каждой итерации, и у каждой функции из цикла своя i.
  • var даёт одну переменную на весь цикл, поэтому функции, вызванные после цикла, видят её последнее значение.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом