Таймеры и потеря контекста в JavaScript

При использовании таймеров в обработчиках событий нас поджидают проблемы с потерей контекста. Давайте посмотрим на примере.

Пусть у нас есть инпут:

<input id="elem" value="text">

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

let elem = document.querySelector('#elem'); elem.addEventListener('click', function() { setInterval(function() { console.log('!!!'); // что-нибудь выводим в консоль }, 1000); });

Пока все работает верно. Но пусть теперь мы хотим выводить в консоль value нашего инпута - нас ждет сюрприз: в консоль будет выводится undefined:

elem.addEventListener('click', function() { setInterval(function() { console.log(this.value); // будет выводится undefined }, 1000); });

Все дело в том, что у нас получается функция в функции: есть внешняя анонимная функция, которая вызывается по клику и внутренняя анонимная функция, которую запускает таймер. Во внешней функции this указывает на инпут, но во внутренней - нет. Имеет место потеря контекста.

Почему выводится undefined, а не вываливается ошибка в консоль, как это было в предыдущих уроках? Потому что this внутри функции, вызываемой через setInterval, указывает на window.

Это значит, что мы пытаемся прочитать свойство value у объекта window, вот так: window.value, а такого свойства в нем нет, и мы получаем undefined (не ошибку).

Поправим проблему введением self:

elem.addEventListener('click', function() { let self = this; setInterval(function() { console.log(self.value); }, 1000); });

Пусть дан такой код:

<input type="button" id="elem" value="1"> let elem = document.querySelector('#elem'); elem.addEventListener('click', function() { setInterval(function() { this.value = Number(elem.value) + 1; }, 1000); });

Автор кода хотел, чтобы по нажатию на кнопку, значение этой кнопки каждую секунду увеличивалось на 1. Однако, по нажатию на кнопку вообще ничего не происходит. Исправьте ошибку автора кода. Напишите текст, в котором вы дадите объяснение автору кода, почему возникла его ошибка.

Другие способы решить проблему

Конечно же, можно использовать все известные вам способы решения проблем с контекстом. Например, можно использовать стрелочную функцию:

elem.addEventListener('click', function() { setInterval(() => console.log(this.value), 1000); });

Исправьте проблему предыдущей задачи через стрелочную функцию.

Решение проблемы через замыкание

Можно также решить проблему с контекстом, используя замыкание:

let elem = document.querySelector('#elem'); elem.addEventListener('click', function() { function func(self) { return function() { console.log(self.value); } } setInterval(func(this), 1000); });

Можно вообще не использовать именованную функцию, а сделать вызов функции на месте. В этом случае мы получим более изящное, но менее понятное решение:

let elem = document.querySelector('#elem'); elem.addEventListener('click', function() { setInterval((function(self) { return function() { console.log(self.value); } })(this), 1000); });

Объясните принцип работы приведенного мною кода.