У меня есть простая программа, которая выглядит так:
console.info("Start of the program execution!");
setTimeout(() => {
console.info("I ran after a second!");
}, 1000);
console.info("The end of the file!");
Тогда у меня возникает вопрос: где находится анонимная функция, пока работает таймер и функция не помещается в стек вызовов?
Если бы у меня было обычное определение функции, код выглядел бы так:
function doSth()
{
console.info("I ran after a second!");
}
console.info("Start of the program execution!");
setTimeout(doSth, 1000);
console.info("The end of the file!");
тогда функция doSth()
все еще будет жить в глобальной памяти после того, как стек вызовов станет пустым, и, таким образом, setTimeout()
сможет использовать свою ссылку для вызова ее после установленного времени. Эта договоренность имеет для меня смысл. Проблемы возникают, когда я использую анонимную функцию и делаю некоторые наблюдения.
Для некоторых наблюдений я сначала поместил отладчик в пятую строку программы; console.info("The end of the file!");
. Затем я смотрю на Scope
Инструментов разработчика. Анонимной функции нигде не видно.
Для следующего наблюдения я поместил отладчик в console.info()
внутри функции setTimeout()
, которая находится в третьей строке программы. Тогда действительно, в стеке вызовов есть анонимная функция, когда мы ее запускаем.
Таким образом, эти два наблюдения приводят меня в замешательство. Если анонимная функция не присутствовала в глобальной памяти ПОСЛЕ вызова setTimeout()
, то как ее можно поместить в стек вызовов, когда setTimeout()
завершит свою работу? Функция «идет» с setTimeout()
в домен webAPIs
, а затем возвращается оттуда?
Функция не обязательно должна находиться в какой-либо области видимости. Это просто объект, на который ссылается объект таймаута.
Это ничем не отличается от передачи безымянного массива или объекта в функцию, которая затем сохраняет эти данные в одной из своих локальных переменных.
@Barmar Имеет смысл. У меня вопрос: где хранится функция (или функциональный объект, как вы ее называете)? Я не вижу его нигде в глобальной памяти, и вдруг он появляется в стеке вызовов!
Цикл событий содержит список объектов таймера в некоторой внутренней переменной. Объекты таймера содержат ссылки на свои функции обратного вызова. Этот список не отображается ни в одном общедоступном интерфейсе.
@Бармар Опять! Согласованный! «Объекты таймера содержат ссылки на свои функции обратного вызова». Конечно, они должны это сделать. Но где эта функция в глобальной памяти? Объект таймера содержит ссылки. Ссылки куда? Я не вижу этого в памяти.
Можно сказать, что он просто сохраняется в куче, а затем на него делается ссылка.
логически, код остается текстом, помещенным «в режим ожидания» во время задержки, и этот код появляется только тогда, когда о нем позаботится интерпретатор JS.
@Keith Это имеет смысл. Не могли бы вы предоставить дополнительную информацию о том, как setTimeout()
работает под капотом? Знает ли setTimeout()
, где в куче находится функция? Все ли webAPIs
«знают» об объектах, которые мы им предоставляем, и их фактическом положении в куче?
«Анонимной функции нигде не видно». - куда ты смотрел? Если бы вы сделали снимок кучи, вы бы его нашли.
«Все ли веб-API «знают» об объектах, которые мы им предоставляем, и об их фактическом положении в куче?» - они знают и заботятся не больше, чем другие API. Они содержат ссылку, и эта ссылка помечается как корень сборки мусора, пока она хранится (асинхронной) реализацией веб-API.
Вы можете себе представить, что setTimeout()
и setInterval()
реализованы примерно так:
class Timer {
static allTimers = {};
static timerId = 0;
constructor(timeout, func, interval) {
this.timerId = ++timerId;
this.timeout = timeout;
this.func = func;
this.interval = interval;
this.expire_time = Date.now() + timeout;
this.allTimers[timerId] = this;
addTimerToEventLoop(this);
}
clear(id) {
let timer = this.allTimers[id];
if (timer) {
removeTimerFromEventLoop(timer);
}
}
function setTimeout(timeout, func) {
let newTimer = new Timer(timeout, func, false);
return newTimer.timerId;
}
function setInterval(timeout, func) {
let newTimer = new Timer(timeout, func, true);
return newTimer.timerId;
}
Функции addTimerToEventLoop()
и removeTimerFromEventLoop()
представляют собой внутренний интерфейс с циклом событий, который заставляет их запускаться по расписанию.
Функции хранятся в свойстве func
каждого объекта Timer
. Если бы класс Timer
действительно был виден пользовательскому коду, а не был внутренним для движка JavaScript, вы могли бы найти его в Timer.allTimers
.
Стандарт HTML определяет шаги инициализации setTimeout
. Он включает в себя определение задачи на шаге 5:
Пусть задача — это задача, выполняющая следующие подэтапы:
[1. ... 2. ...]
- Если обработчик является функцией, вызовите обработчик
Другими словами, задача, созданная setTimeout
, содержит ссылку на обратный вызов (обработчик) внутри объекта задачи — независимо от того, является ли он анонимным или именованным. По общему принципу JavaScript, если у объекта есть хотя бы одна ссылка, он не будет кандидатом на сбор мусора.
По истечении периода ожидания выполняется шаг, называемый CompleteStep, описанный в шаге 11 той же процедуры:
- Пусть завершениеStep будет шагом алгоритма, который ставит глобальную задачу в очередь в источнике задачи таймера, заданном глобально для запуска задачи.
Фактическое выполнение этого шага алгоритма описано в разделе выполнить шаги после таймаута.
Но самое главное, чтобы агент (хост, веб-браузер), предоставляющий setTimeout
, имел ссылку на функцию обратного вызова во время вызова setTimeout
и возможного выполнения обратного вызова.
Когда движок оценивает выражение, в котором есть вызов функции (в данном случае вызов setTimeout
), сначала оцениваются аргументы и только потом вызывается эта функция. В вашем примере кода порядок оценки следующий:
Функция обратного вызова (объект):
() => {
console.info("I ran after a second!");
}
Именно на этом этапе выделяется функциональный объект. Затем:
1000
Это простое примитивное значение.
Выражение setTimeout
:
setTimeout(() => {
console.info("I ran after a second!");
}, 1000);
В этот момент вызывается setTimeout
, и он получает ссылку на объект функции, созданный на шаге 1, и значение примитива 1000.
Это не setTimeout
, который создает этот объект обратного вызова в памяти. Это происходит в обычном порядке оценки и не относится только к setTimeout
.
Если setTimeout
ничего не будет делать с первым полученным аргументом, то, когда он вернется, объект функции обратного вызова, созданный на шаге 1, больше не будет иметь ссылки на него. Его память может быть освобождена в любой момент при запуске сборщика мусора. Но поскольку setTimeout
копирует ссылку на функцию в нелокальную память (в объект задачи), эта функция обратного вызова остается доступной для последующего выполнения: на нее есть ссылка, которая переживет вызов setTimeout
.
Итак, если функция уже существует в глобальной памяти, то объект задачи имеет ссылку на эту функцию, а если это анонимная функция, СНАЧАЛА она сохраняется в куче с помощью setTimeout()
, а ЗАТЕМ ее ссылка сохраняется в объект задачи. Это то, что ты пытаешься сказать? Кто выполняет работу по хранению этой функции в куче? Это JavaScript или setTimeout()
?
Функция уже существует в памяти до вызова setTimeout
. Это общий принцип языков программирования: если есть f(x, y)
, то оценивается x
, затем оценивается y
и затем вызывается f
. Результатом вычисления выражения функции является объект функции, который размещается в куче, как и любой другой объект JavaScript. setTimeout
вызывается и получает ссылку на эту память. Эту ссылку он затем копирует в свое свойство объекта задачи, чтобы она не собиралась мусором при возврате setTimeout
.
Ого! Итак, когда я запустил setTimeout()
, анонимная функция находилась в локальной области setTimeout()
, которая затем добавила эту функцию из своей локальной области в объект задачи, и, таким образом, даже когда setTimeout()
выскочила из стека, таким образом, имея свою локальную область видимости. область действия также была уничтожена, функция все еще находилась в объекте задачи и, следовательно, на нее ссылались оттуда, когда обратный вызов вызывался после установленного времени. Это именно то? Это ДЕЙСТВИТЕЛЬНО имеет абсолютный смысл. Большое спасибо. Я приму ответ сейчас, потому что на все мои вопросы были даны ответы и мои запросы выполнены. :-)
Да, это так, как вы пишете.
Нет существенной разницы между именованной и анонимной функцией. В любом случае вы просто передаете объект функции
setTimeout()
, и он сохраняет этот объект.