Задать вопрос
@Itvanya

Замыкания внутри циклов в JavaScript. Какова механика работы?

Доброго времени суток, программисты. Уже давно пишу на js и, естественно, в курсе сюрпризов JavaScript относительно замыкание внутри closures. Хочу понять механику того, почему JavaScript на этапе итерации теряет локальную переменную i, возвращая в итоге, допустим, при i < 10, 10-ку. Что, собственно, и показано ниже. В данном случае, на сколько я понимаю, асинхронный таймаут медленнее цикла, поэтому к моменту его исполнения таймаута i = 10.
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}


Теперь несколько примеров, которые делают так, чтобы все было ок :
1) Пожалуй, самый распространённый. В данном случае анонимная функция запускается моментально, передавая в замыкание текущий i, используя обёртку.
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}


2) По-идее, то же самое.
for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}


3) Штука, которой пользуюсь я. Написал сам, выглядит более интуитивно, особенно проще воспринимается в больших проектах на чисто ванильном js.
function count(i) {
  return setTimeout(function(){
    console.log(i);
  }, 1000);
}

for ( var i = 0 ; i < 10 ; i++ ) {
  count(i);
}


4) Еще один безумный случай, написанный мной просто для фана. Работает аналогично и быстрее остальных случаев.
setTimeout(function(){ 
  for(var i = 0; i < 10; console.log(i++)) {}
}, 1000)


Выше перечислены не все возможные приёмы использование. Также можно сделать подобную операцию, используя call, apply, bind, передавая в функцию-обёртку значения из цикла.
Основная суть моего вопроса : как вы видите механику работы замыкания внутри цикла? Какие еще вы знаете приёмы работы с ними?
  • Вопрос задан
  • 4678 просмотров
Подписаться 2 Оценить 1 комментарий
Решения вопроса 1
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
в курсе сюрпризов JavaScript относительно замыкание внутри closures.

Если для вас это все еще сюрпризы - рекомендую перештудировать какой-нибудь туториал или документацию. В JS все как-будто асинхронно, на самом деле там просто есть event loop.

Давайте разберемся на примере:

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

Что у нас тут будет происходить. Мы берем цикл и 10 раз создаем отложенный вызов. Что при этом происходит... давайте представим себе что в JS все разбито на кадры. Цикл - один кадр, и пока он не отработает, какой бы он длинный не был - новый кадр так же не отработает. setTimeout не просто выполняет код с задержкой, через 1000 милисекунд в event loop будет добавлен очередной кадр. И любой "асинхронный" вызов так же просто добавляет кадр в event loop. Внутри же event loop все выполняется синхронно и по порядку.

Так вот, на момент, когда закончит выполнение кадр с циклом, значение i уже будет установлено в 9. А как мы выяснили ранее, JS ничего более не будет выполнять до этого момента. Посему отложенный код выведет нам одно и то же значение так как все они ссылаются на одну и ту же переменную.

Теперь, что происходит тут:
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}


Да собственно то же самое. Только за счет того что мы при публикации setTimeout используем замыкание и передаем i в качестве аргумента, срабатывает механизм называемый copy-on-write. То есть в замыкании не i а его копия (когда цикл перезаписывает значение i - все кто ссылался на это значение не по ссылке, копируют себе переменную, для простоты можно просто думать что каждый раз когда вы передаете что-то не по ссылке, происходит копирование).

Так как у каждого кадра, выполняемого в setTimeout есть своя копия i с нужным значением - все будет хорошо.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@abba-best
Нужно использовать let вместо var
for(let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы