Функции setTimeout, setInterval и clearInterval

Рассмотрим две часто используемые в JavaScript функции: setTimeout и setInterval. Первая позволяет выполнять произвольную функцию через определенный период времени и останавливается, а вторая – многократно выполняет функцию через указанный интервал.

Функция setTimeout имеет такой синтаксис:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

  • func|code – ссылка на функцию или строка кода, которая должна выполниться;
  • delay – задержка перед вызовом в миллисекундах (1000 мс = 1 сек);
  • arg1, arg2, … - возможные аргументы для запускаемой функции (не поддерживаются в старых браузерах IE9-).

Например, запустим функцию

function createMsg() {
    let msg = "Hello";
    console.log(msg);
}

через две секунды:

setTimeout(createMsg, 2000);

Обратите внимание, мы передаем в setTimeout ссылку на функцию, поэтому круглые скобки после ее имени не пишем. Далее указан интервал запуска 2000 мс = 2 сек.

Если функции нужно передать какие-либо аргументы, то это делается так:

function createMsg(msg) {
    console.log(msg);
}
 
setTimeout(createMsg, 2000, "timeout hello");

Во всех примерах функция запускалась только один раз спустя 2 сек. Поэтому, setTimeout в таких случаях можно не останавливать – она автоматически завершится после однократного запуска функции. Но, бывают случаи, когда setTimeout нужно завершить до ее запуска. Например, с сервера загружаются сообщения: они могут скачаться быстро, если составляют небольшой размер, но могут скачиваться долго, в случае передачи фотографии или видео. Скрипт на стороне клиента не может заранее знать об объеме принимаемых данных, поэтому для подстраховки запускается отложенный вызов функции, которая на экране отображает анимированное изображение, показывающее процесс загрузки. Почему вызов отложенный? Потому что при быстрой загрузке ничего показывать не нужно, пользователь этого не заметит. И в этом случае нужно прервать функцию setTimeout, чтобы на экране ничего не отображалось. Это может выглядеть так:

function downloadMsg() {
    let idLoading = setTimeout(function() {
         console.log("Идет загрузка данных...");
    }, 500);
 
    setTimeout(function() {
         clearTimeout(idLoading); //останавливаем setTimeout после загрузки данных
         console.log("Данные загружены");
    }, 2000);     //задержка: имитация загрузки с сервера
}
 
downloadMsg();

В setTimeout также можно передавать строки с выражением, которое нужно выполнить:

setTimeout("alert('Привет')", 1000);

Но вместо них лучше использовать функции, в том числе и стрелочные:

setTimeout(() => alert('Привет'), 1000);

Если в setTimeout не указывается второй аргумент, то задержка принимается равной 0 и функция, переданная в setTimeout запускается настолько быстро, насколько это возможно. Но планировщик будет вызывать функцию только после завершения выполнения текущего кода. Например:

setTimeout(() => console.log("Мир"));
console.log("Привет");

Увидим сообщения: «Привет» и «Мир».

Функция setInterval

Вторая функция setInterval имеет такой же синтаксис, что и функция setTimeout, но выполняет переданную ей функцию многократно с заданным интервалом, пока не будет остановлена:

function createClock(seconds) {
    let sec = seconds;
 
    return function() {
         sec++;
         console.log("Прошло " + sec + " секунд(а)");
    }
}
 
let clock = createClock(0);
setInterval(clock, 1000);

В этом примере мы, используя замыкание, создали функцию clock и далее, запускаем ее с интервалом в 1 секунду. Она будет выполняться до тех пор, пока мы ее не остановим:

let idClock = setInterval(clock, 1000);
setTimeout(function() {
    clearInterval(idClock)
}, 5000);

Остановку делаем с помощью отложенного вызова анонимной функции через 5 секунд, в которой вызываем clearInterval с указанием idClock.

Во всех рассмотренных примерах на передаваемые функции в setTimeout или setInterval автоматически создавались ссылки и хранились в планировщике. Это предотвращало удалений функций сборщиком мусора, даже если на них не было явных внешних ссылок в скрипте. Например:

setTimeout(() => console.log("Мир"));

На стрелочную функцию ссылается планировщик до момента ее выполнения. А в

let idInterval = setInterval(function() {}, 1000);

анонимная функция остается в памяти до тех пор, пока не будет вызван

clearInterval(idInterval);

Здесь стоит иметь в виду, что передаваемая функция ссылается на внешнее лексическое окружение, поэтому пока она существует, внешние переменные тоже существуют. Они могут занимать больше памяти, чем сама функция. Поэтому, если регулярный вызов функции больше не нужен, то лучше отменить его.

Потеря this

Одной из проблем использования setTimeout и setInterval является потеря this при вызове методов объектов. Например:

let car = {
    model: "bmw",
 
    showModel() {
         console.log( this.model );
    }
};
 
setTimeout(car.showModel, 1000);

В консоле мы увидим undefined. Почему? Дело в том, что здесь теряется контекст при вызове функции. Это эквивалентно вот такому вызову:

let show = car.showModel;
show();

И, так как в JavaScript this вычисляется динамически при каждом вызове функции, то здесь JavaScript-машина просто не может связать функцию show с объектом car.

Исправить ситуацию можно несколькими способами. Первый:

setTimeout(function() {car.showModel();}, 1000);

вызвать car.showModel через анонимную функцию-обертку. Здесь мы при вызове явно указываем объект car, поэтому this будет определен корректно. Второй способ – использовать метод bind, о котором мы говорили на предыдущем занятии:

let show = car.showModel.bind(car);
setTimeout(show, 1000);

Этот способ предпочтительнее использовать на практике, так как после вызова bind он не зависит от значения переменной car. Если она будет изменена, то функция show все равно корректно вызовется, а вот в первом случае мы бы получили ошибку. Вот это следует иметь в виду при реализации отложенных вызовов.

И в заключение занятия пару слов об особенностях работы стрелочных функций. Важно знать, что у стрелочных функций нет своего контекста выполнения (лексического окружения), а значит, нет и своего this. В некоторых случаях этот момент имеет ключевое значение, например:

let group = {
    title: "ТКбд-11",
    students: ["Иванов", "Петров", "Сидоров"],
 
    showList() { 
         this.students.forEach(
             student => console.log(this.title + ': ' + student)
         );
    }
};
 
group.showList();

Здесь метод forEach при вызове обычной функции устанавливает контекст this=undefined, но при использовании стрелочной функции контекст неизбежно берется от функции showList и this=group. Например, если заменить стрелочную функцию на обычную, то получим ошибку:

function(student) {console.log(this.title + ': ' + student);}

с сообщением undefined не имеет свойства title.

Видео по теме