Рефакторинг моего ответа из-за ваших комментариев ниже.
Прежде чем мы начнем, нам нужно рассмотреть несколько терминов:
Контекст выполнения - Проще говоря, это «среда», в которойФункция выполняется в. Например, когда наше приложение запускается, мы запускаем «глобальный» контекст выполнения, когда мы вызываем функцию, мы создаем новый контекст выполнения (вложенный в глобальный).
Каждый контекст выполнения имеет переменную-окружение (область действия) и, конечно же, тело функции (это «команды»).
Стек вызовов - Чтобы отслеживать, в каком контексте выполнения мы находимся, и какие переменные доступны для нас, каждый контекст выполнения передается в вызов.стек, когда функция возвращает ее извлеченную из стека вызовов, и ее среда помечается как сборщик мусора (освобождает память), за исключением одного исключения, которое мы выясним позже.
API веб-браузера и цикл обработки событий - JavaScript является однопоточным (давайте назовем его потоком для простоты), но иногда нам нужно обрабатывать асинхронные действиятакие как click-events, xhr и timers.
Браузер предоставляет их через свой API, addEventListener
, XHR
/ fetch
, setTimeout
и т.д ...
Самое классное то, что онбудет запускать его в другом потоке, чем поток JavaScript.Но как браузер может запустить наш код в главном потоке?через обратные вызовы, которые мы предоставляем ему (как вы сделали с setTimeout
).
Хорошо, когда он запустит наш код?нам нужен предсказуемый способ запуска нашего кода.
Заходит в цикл обработки событий и в Callback-Que, браузер отправляет каждый обратный вызов в эту очередь (между прочим, обещания переходят в другую очередь с более высоким приоритетом) и цикл обработки событийнаблюдает за стеком вызовов, когда когда-либо стек вызовов пуст и больше нет кода для запуска в глобальном , цикл обработки событий захватит следующий обратный вызов и отправит его в стек вызовов.
Закрытие - Проще говоря, это когда функция обращается к своей лексической (статической) области видимости, даже если она выходит за ее пределы.это будет яснее позже.
В примере # 1 - Мы запускаем цикл в глобальном контексте выполнения , создавая переменную i
и меняя ее на новое значение на каждой итерации, одновременно передавая 5обратные вызовы в браузер (через setTimeout
API).
Цикл обработки событий не может отправить эти обратные вызовы обратно в стек вызовов, поскольку он еще не пуст.однако, когда цикл завершился, стек вызовов пуст, и цикл обработки событий возвращает к нему наши обратные вызовы, каждый обратный вызов получает доступ к i
и печатает последнее значение 5
(закрытие, мы получаем доступ к среде i
послеон должен был быть уничтожен).причина этого в том, что все обратные вызовы были созданы в одном контексте выполнения, поэтому они ссылаются на один и тот же i
.
В примере # 2 - Мы запускаем цикл в глобальном контексте выполнения , создавая новую функцию ( IIFE ) на каждой итерации,таким образом создавая новый контекст исполнения.это создаст копию i
внутри этого контекста выполнения, а не в глобальном контексте, как раньше.Внутри этого контекста выполнения мы отправляем обратный вызов через setTimeout
, как и прежде, чем цикл обработки событий ожидает, пока цикл не завершится, поэтому стек вызовов будет пустым и перенесет следующий обратный вызов в стек.но теперь, когда выполняется обратный вызов, он получает доступ к своему контексту выполнения, где он был создан, и печатает i
, который никогда не изменялся глобальным контекстом.
Итак, в основном у нас есть 5 контекстов выполнения (без глобального), каждый из которых имеет свой i
.
Надеюсь, теперь это стало понятнее.
Я очень рекомендую посмотреть это видео о цикле событий.