setTimeout()
кажется простой функцией: подожди — и выполни. Но за этой простотой прячется куча нюансов. Почему setTimeout(…, 0)
не срабатывает сразу? Почему this
вдруг становится undefined? Почему try-catch
не ловит ошибку внутри таймера? И как неосторожное обращение с таймерами может повесить весь интерфейс?
В этой статье разберёмся, что такое setTimeout
, как он работает, как правильно его использовать и где чаще всего возникают сложности.
Что такое setTimeout()
setTimeout()
— это встроенная функция JavaScript, которая позволяет запустить любой код с задержкой во времени. Она не блокирует выполнение скрипта и не останавливает поток — просто откладывает выполнение указанной функции на нужное количество миллисекунд.
По умолчанию JavaScript работает синхронно: выполняет команды сверху вниз, построчно, одна за другой. Но setTimeout()
— асинхронный: он ставит задачу в очередь и продолжает выполнение остального кода. Когда заданное время истечёт, то браузер (или движок Node.js) вернётся и выполнит отложенный код.
Проще говоря, когда мы пишем setTimeout
, то говорим: «Подожди N миллисекунд, а потом выполни вот эту функцию». Это не «пауза в коде», а именно «поставь это на потом».
Это основа всей отложенной логики: уведомления, анимации, подсказки, отложенные действия, заглушки, дебаунсы, фейковые загрузчики — всё это делается через setTimeout
.
Синтаксис и параметры
Функция setTimeout() используется, чтобы запланировать выполнение другой функции через определённое время (в миллисекундах).
Базовый синтаксис выглядит так:
setTimeout(callback, delay, param1, param2)
Здесь мы прописываем:
callback
— функция, которую нужно выполнить;delay
— задержка в миллисекундах (1000 мс = 1 секунда);param1, param2, ...
— необязательные аргументы, которые передаются в колбэк.
Параметры функции
Callback
— это функция, которая будет вызвана после указанной задержки. Можно передавать как обычную, так и стрелочную функцию:
setTimeout(function () {
console.log('Через 2 секунды');
}, 2000);
Delay
— время задержки в миллисекундах. То есть 1000 — это 1 секунда. Можно указать 0, и тогда функция попадёт в конец очереди (как именно это работает, разберём позже).
Дополнительные аргументы — можно передать значения, которые попадут в callback
в виде аргументов:
function greet(name) {
console.log(`Привет, ${name}`);
}
setTimeout(greet, 2000, 'бро'); // Привет, бро (через 2 секунды)
Возвращаемое значение
Когда вы вызываете setTimeout()
, он не просто запускает таймер — он ещё и возвращает число. Это timeoutID
— уникальный идентификатор таймера, с помощью которого его можно отменить через clearTimeout()
.
setTimeout(() => {
console.log("Через 2 секунды");
}, 2000);
// 5
// Через 2 секунды
Вот это 5 и есть ID таймера. Чтобы не видеть ID в консоли, можно сохранить его в переменную:
const id = setTimeout(() => {
console.log("Через 2 секунды");
}, 2000);
Так в консоли ничего лишнего не появится.
Как работает setTimeout()
Функция setTimeout()
не просто отложенный запуск. Это часть event loop — цикла событий в JS, и она ведёт себя по своим правилам. Код срабатывает не раньше чем через указанную задержку, но точный момент зависит от текущей загрузки потока.
Задержка в миллисекундах
SetTimeout()
принимает задержку во втором параметре — в миллисекундах. Но даже если вы поставите 0, функция не выполнится мгновенно. Она встанет в очередь и сработает, когда движок освободится.
console.log("Сначала");
setTimeout(() => {
console.log("setTimeout с 0");
}, 0);
console.log("Потом");
// Сначала
// Потом
// setTimeout с 0
timeoutID и его использование
Мы уже говорили, что когда вы вызываете setTimeout()
, он возвращает уникальный идентификатор — timeoutID
. Его можно сохранить в переменную, и в дальнейшем использовать:
const timerId = setTimeout(() => {
console.log("Прошло 5 секунд");
}, 5000);
timeoutID
— уникальный идентификатор таймера, и браузер не даст два одинаковых ID одновременно. Идентификатор живёт в пределах одного окружения — одного окна или воркера. И как только таймер выполнится или отменится, ID может быть переиспользован системой для нового таймера (но не раньше).
Отмена setTimeout с помощью clearTimeout()
Если вы передумали или не хотите, чтобы функция выполнялась, — отмените таймер через clearTimeout(timeoutID)
:
const id = setTimeout(() => {
console.log('Я не должен выполниться!');
}, 5000);
clearTimeout(id); // И всё, никаких выполнений
Если передать несуществующий ID, ничего страшного не произойдёт. Ошибки не будет, clearTimeout()
просто ничего не сделает. Это удобно, если не уверены, есть ли у вас активный таймер.
👉 setTimeout()
и setInterval()
используют один и тот же пул ID. Теоретически можно отменить интервал через clearTimeout()
, но лучше не хулиганить и использовать правильную пару clearInterval()
.
Использование setTimeout в реальных проектах
setTimeout()
используется в живых интерфейсах, при взаимодействии с пользователем, для отложенных действий, а иногда и для обхода странностей браузеров. Разберёмся на примерах.
Пример: запланировать выполнение функции
Допустим, мы хотим показать на сайте рекламный баннер или приветствие и убрать его через пару секунд. Делается это так:
// Ждём полной загрузки HTML-документа (DOM-дерева)
window.addEventListener("DOMContentLoaded", () => {
// Находим элемент баннера по классу
const banner = document.querySelector(".promo-banner");
// Показываем баннер (если он скрыт стилями по умолчанию)
banner.style.display = "block";
// Через 2 секунды (2000 миллисекунд) скрываем баннер
setTimeout(() => {
banner.style.display = "none";
}, 2000);
});
Такой приём часто используют для временных сообщений, баннеров или экранов-заставок.
Пример: использование аргументов с setTimeout()
Допустим, после входа на сайт мы хотим не сразу, а с небольшой задержкой показать пользователю персональное приветствие. Это может выглядеть так:
// Функция, которая вставляет имя пользователя в приветствие и делает блок видимым
function showGreeting(name) {
const msg = document.querySelector(".greeting");
msg.textContent = `Привет, ${name}!`;
// допустим, у блока по умолчанию opacity: 0
msg.style.opacity = 1;
}
// Откладываем вызов функции на 1,5 секунды и передаём аргумент "Джеймс"
setTimeout(showGreeting, 1500, "Джеймс Сандерленд");
Функция setTimeout ждёт 1,5 секунды, после этого вызывает showGreeting()
, передавая туда строку с именем пользователя, функция обновляет содержимое блока .greeting
и делает его видимым.
Хорошо работает, когда нужно сделать интерфейс чуть дружелюбнее — без резких появлений.
Пример: передача строки вместо функции
у setTimeout() есть альтернативный синтаксис: вместо функции можно передать строку с кодом. Она будет скомпилирована и выполнена, когда сработает таймер:
setTimeout("alert('Опасненько')", 1000);
По сути, это работает точно так же, как eval()
— а значит, несёт те же риски. eval()
— это функция, которая берёт строку и выполняет её как код. Очень мощная штука, но с ней легко запустить в проект дичь, особенно если в строке окажется пользовательский ввод. Это потенциальная дыра в безопасности и источник XSS-атак.
⚠️ Поэтому, если нужно запланировать действие — передавайте функцию, а не строку.
Пример: задержка с нулевым значением
Иногда setTimeout(fn, 0)
используют, чтобы дать браузеру отрисовать изменения, прежде чем выполнять какую-то тяжёлую или блокирующую операцию.
Допустим, у нас есть кнопка, по нажатию на которую показывается индикатор загрузки, а потом срабатывает какая-то тяжёлая функция:
button.addEventListener("click", () => {
loader.style.display = "block"; // Показываем лоадер
// Плохо: тяжёлая функция блокирует отрисовку лоадера
doHeavyStuff();
});
В этом случае loader
может не успеть появиться на экране — потому что движок JavaScript сразу прыгнет в doHeavyStuff
и браузер даже не дойдёт до отрисовки. Чтобы этого избежать, используем setTimeout
с задержкой 0
:
button.addEventListener("click", () => {
// Показываем загрузчик
loader.style.display = "block";
// Даём движку отрисовать загрузчик, а потом запускаем тяжёлый код
setTimeout(() => {
doHeavyStuff();
}, 0);
});
Теперь всё работает как надо: сначала сработает индикатор загрузки, а только потом запустится сложная операция.
Пример: try-catch в setTimeout()
Многие интуитивно пытаются ловить ошибки внутри setTimeout
вот так:
try {
setTimeout(() => {
throw new Error("Что-то пошло не так");
}, 1000);
} catch (err) {
console.log("Не отловит ", err.message);
}
Но так ничего не выйдет, поскольку setTimeout
асинхронный. Ошибка произойдёт уже после того, как try-catch
отработал и очистился стек вызовов.
Чтобы правильно ловить ошибки внутри setTimeout
, нужно обернуть код внутри самой функции в try...catch
:
setTimeout(() => {
try {
// потенциально опасный код
throw new Error("Ошибка внутри таймера");
} catch (err) {
console.error("Поймали внутри setTimeout:", err.message);
}
}, 1000);
Такой приём особенно полезен, если вы запускаете в setTimeout
нестабильный код или обрабатываете пользовательские данные.
Пример: this в setTimeout()
В JavaScript значение this
зависит от того, как вызвана функция. И это часто становится ловушкой, особенно в связке с setTimeout
.
Допустим, вы пишете компонент, который должен показать сообщение через пару секунд:
const popup = {
message: "Привет, я раздражающее окно!",
show() {
// Здесь обычная функция теряет контекст
setTimeout(function () {
// undefined
console.log(this.message);
}, 2000);
},
};
popup.show();
Почему this.message
не сработало? Потому что внутри setTimeout
обычная функция создаёт свой контекст. А значит, this
указывает не на popup
, а на глобальный объект (window
в браузере).
Чтобы исправить это, то внутри setTimeout
при обращении к this
используем стрелочные функции — это и современно, и безопасно. Стрелочные функции не имеют своего this, а берут его из внешнего контекста.
const popup = {
message: "Привет, я раздражающее окно!",
show() {
setTimeout(() => {
console.log(this.message); // Всё работает!
}, 2000);
},
};
popup.show();
Есть и другие способы, но обычно хватает стрелочных функций.
Практический пример: дебаунс
В реальных проектах не всегда нужно сразу выполнять функцию при каждом действии пользователя. Если он вводит текст в поисковую строку, то отправлять запрос к серверу на каждую нажатую клавишу плохо.
В таких ситуациях используют дебаунс (debounce) — приём, при котором выполнение функции откладывается, пока пользователь не прекратит активные действия (печатать или скроллить).
Это удобно, когда у нас есть автокомплит в поиске, фильтрация товаров по вводу, ресайз окна и скроллы (чтобы не дёргать логику на каждое движение).
Как работает debounce:
- принимает другую функцию и таймаут (например, 300 мс);
- откладывает её выполнение;
- если функция вызывается повторно до истечения таймаута — старый вызов отменяется.
В результате функция срабатывает только один раз и только после того, как пользователь остановится.
function debounce(fn, delay) {
// хранит ID текущего таймера
let timeoutId;
// возвращаем новую функцию-обёртку
return function (...args) {
// каждый раз при вызове отменяем предыдущий таймер
clearTimeout(timeoutId);
// запускаем новый таймер
timeoutId = setTimeout(() => {
// вызываем оригинальную функцию с теми же аргументами и сохранённым контекстом
fn.apply(this, args);
// задержка перед выполнением (в миллисекундах)
}, delay);
};
}
В итоге у нас снижается нагрузка на сервер и улучшается UX — становится меньше дёрганий и лагов.
Сравнение setTimeout и setInterval
Обе функции позволяют запускать код с задержкой. Но работают они по-разному:
setTimeout(fn, delay)
— выполнитfn
один раз через указанное время.setInterval(fn, delay)
— будет выполнятьfn
периодически, каждые delay миллисекунд, пока не остановим вручную.
Кажется, просто. Но в реальных проектах setInterval
может подкинуть сюрпризов. Разберём пару примеров.
Во вкладках не в фокусе таймеры замедляются
Если пользователь свернёт вкладку, браузер начнёт экономить ресурсы: setInterval
будет срабатывать реже, а иногда пауза может быть в разы длиннее, чем ожидалось.
Например, вы показываете таймер до конца акции:
setInterval(() => {
secondsLeft--;
updateBanner(secondsLeft);
}, 1000);
В фоновом режиме secondsLeft
будет уменьшаться нерегулярно — и по возвращении пользователь может увидеть, что таймер сломан. Поэтому вместо этого лучше использовать реальную дату и время.
setInterval не ждёт завершения функции
Допустим, нам по какой-то причине нужно обращаться к серверу каждую секунду:
setInterval(() => {
// запрос к серверу
fetch('/api/update');
}, 1000);
Проблема в том, что если сервер вдруг начнёт отвечать дольше (например, 1,5 секунды), то новые вызовы fetch()
начнут накладываться друг на друга. Вы ещё не получили ответ — а уже отправили следующий запрос. Так накапливаются «хвосты» запросов, и сайт начинает тормозить.
Поэтому лучше использовать setTimeout
в цепочке, чтобы следующий вызов происходил только после завершения предыдущего:
function poll() {
fetch('/api/update')
.finally(() => {
// запланируем новый вызов через секунду
setTimeout(poll, 1000);
});
}
poll();
В критичных сценариях вообще лучше избегать setInterval
. Если важна точность или стабильность (отсчёт времени, опрос сервера, анимация), setTimeout
даст больше контроля.
Нюансы использования setTimeout
На первый взгляд кажется, что setTimeout
— простая и удобная штука. Он не блокирует поток, позволяет делать паузы и откладывать действия. Но за всей этой простотой стоит очередь задач, приоритеты и ловушки производительности.
Разберём, как это работает.
Асинхронность и синхронность
Как мы уже говорили, JavaScript однопоточный: код исполняется сверху вниз, строка за строкой. setTimeout()
работает иначе: он ставит задачу в очередь и не ждёт её выполнения.
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
Даже с задержкой 0 таймер сработает после основного потока, потому что setTimeout
— это макрозадача. Он попадёт в очередь и будет выполнен, когда стек вызовов освободится.
Это важно, если вам нужно отложить тяжёлую логику, чтобы сначала всё отрисовалось и если мы хотим дать интерфейсу «отдышаться» перед сложной операцией.
Микрозадачи и макрозадачи
Когда мы используем setTimeout
, то добавляем задачу в очередь макрозадач. Это значит, что браузер поставит нашу функцию в очередь и выполнит её только после того, как:
- завершится основной код (тот, что выполняется строка за строкой);
- отработают все микрозадачи.
Микрозадачи — это особый тип асинхронных операций, которые исполняются раньше, чем макрозадачи. К ним относятся, например, метод .then()
у промисов, async/await
(под капотом они тоже используют промисы) и методы вроде queueMicrotask()
. Мы подробно разбирали, как работает Event Loop, микрозадачи и макрозадачи, в статье про async/await
в JavaScript. Обязательно почитайте, если ещё не видели.
Пример:
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
Хотя setTimeout
задан с нулевой задержкой, первым выполнится промис, поскольку он попадает в микрозадачи, а у них выше приоритет.
👉 setTimeout
не гарантирует точную задержку — он только говорит «выполни, когда сможешь, но не раньше чем через X мс». А вот микрозадачи браузер старается выполнить сразу после текущего кода.
Проблемы с производительностью
Рассмотрим пару основных примеров.
Утечки памяти. Если вы создаёте таймер, но не отменяете его, когда он уже не нужен, — он может продолжать жить в памяти, даже если связанный с ним DOM-элемент уже удалён. Особенно актуально для одностраничных приложений и динамических компонентов (в React или Vue).
const timeoutId = setTimeout(() => {
doSomething(); // а элемента на странице уже нет
}, 5000);
// если не вызвать clearTimeout(timeoutId), колбэк всё равно сработает
Всегда вызывайте clearTimeout
, если компонент удаляется или таймер больше не нужен. Это освободит ресурсы и предотвратит неожиданное поведение.
Перегрузка таймерами. Если вы ставите setTimeout
или setInterval
в цикле или запускаете их в ответ на частые события (например, события scroll или mousemove), можно легко создать сотни таймеров, которые браузер попытается отработать одновременно. Это приводит к замираниям интерфейса, ненужной нагрузке на процессор и снижению отзывчивости страницы.
Поэтому используйте дебаунс для «заглушки» частых вызовов:
// Применяем debounce для события scroll
window.addEventListener(
"scroll",
debounce(() => {
// Эта функция не будет вызываться на каждый пиксель прокрутки —
// а только когда прокрутка остановится хотя бы на 300 мс
console.log("Обработка scroll");
}, 300)
);
Это спасёт и интерфейс, и нервы пользователей.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
Вам слово
Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!