JavaScript замыкания внутри циклов — простой практический пример
Описание проблемы
Я столкнулся с проблемой при работе с замыканиями в JavaScript. Я создал несколько функций внутри цикла и сохранил их в массив, но когда я вызываю эти функции, они все выводят одно и то же значение, а именно 3
. Вот пример кода, который я использовал:
var funcs = [];
// создаем 3 функции
for (var i = 0; i < 3; i++) {
// и сохраняем их в funcs
funcs[i] = function() {
// каждая должна выводить свое значение.
console.log("My value:", i);
};
}
for (var j = 0; j < 3; j++) {
// и теперь давайте вызовем каждую, чтобы увидеть
funcs[j]();
}
Вывод получается следующим:
My value: 3
My value: 3
My value: 3
Тогда как мне нужно, чтобы выводилось:
My value: 0
My value: 1
My value: 2
Дополнительные примеры
Такая же проблема возникает и при использовании обработчиков событий:
var buttons = document.getElementsByTagName("button");
// создаем 3 функции в качестве обработчиков событий
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
// каждая должна выводить свое значение.
console.log("My value:", i);
});
}
Или при использовании асинхронного кода, например, с Promises:
// Функция ожидания
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
for (var i = 0; i < 3; i++) {
// Логируем `i`, как только каждый promise будет разрешен.
wait(i * 100).then(() => console.log(i));
}
Проблема также проявляется в циклах for...in
и for...of
:
const arr = [1, 2, 3];
const fns = [];
for (var i in arr) {
fns.push(() => console.log("index:", i));
}
for (var v of arr) {
fns.push(() => console.log("value:", v));
}
for (const n of arr) {
var obj = { number: n }; // или new MyLibObject({ ... })
fns.push(() => console.log("n:", n, "|", "obj:", JSON.stringify(obj)));
}
for (var f of fns) {
f();
}
Вопрос
Какова же правильное решение этой базовой проблемы с замыканиями в JavaScript?
3 ответ(ов)
Другими словами, i
в вашей функции связывается в момент выполнения функции, а не в момент её создания.
Когда вы создаете замыкание, i
ссылается на переменную, определенную во внешнем контексте, а не является копией этой переменной на момент создания замыкания. Значение будет оцениваться в момент выполнения.
Большинство других ответов предлагают решения, основанные на создании другой переменной, значение которой не изменится.
Просто хотел добавить объяснение для ясности. Что касается решения, я бы лично выбрал подход Хартo, так как он является самым понятным способом из представленных здесь вариантов. Любой из приведённых кодов будет работать, но я предпочел бы фабрику замыканий, чем писать кучу комментариев, чтобы объяснить, почему я объявляю новую переменную (как у Фредди или из 1800-х) или использовать странный встроенный синтаксис замыкания (как у apphacker).
Вам нужно понять, что область видимости переменных в JavaScript основана на функциях. Это важное отличие от, скажем, C#, где есть блочная область видимости, и просто копирование переменной внутрь цикла for сработает.
Оборачивание кода в функцию, которая возвращает эту функцию, как в ответе apphacker, решит вашу задачу, так как переменная теперь имеет функциональную область видимости.
Также существует ключевое слово let
, которое позволяет использовать правила блочной области видимости. В этом случае определение переменной внутри цикла for сработает. Однако стоит отметить, что использование let
не всегда является практическим решением из-за вопросов совместимости.
Вот пример кода:
var funcs = {};
for (var i = 0; i < 3; i++) {
let index = i; // добавьте это
funcs[i] = function() {
console.log("My value: " + index); // измените на копию
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
Таким образом, при использовании let
внутри цикла переменная index
будет хранить правильное значение для каждого созданного замыкания.
В этом примере рассматривается распространенная ошибка при использовании замыканий в JavaScript.
Функция определяет новую среду
Рассмотрим следующий код:
function makeCounter()
{
var obj = {counter: 0};
return {
inc: function(){obj.counter ++;},
get: function(){return obj.counter;}
};
}
counter1 = makeCounter();
counter2 = makeCounter();
counter1.inc();
alert(counter1.get()); // возвращает 1
alert(counter2.get()); // возвращает 0
Каждый раз, когда вызывается makeCounter
, создается новый объект {counter: 0}
. Кроме того, создается новая копия obj
, ссылающаяся на этот новый объект. Следовательно, counter1
и counter2
работают независимо друг от друга.
Замыкания в циклах
Использование замыкания в цикле может быть проблематичным.
Рассмотрим такой код:
var counters = [];
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
}
makeCounters(2);
counters[0].inc();
alert(counters[0].get()); // возвращает 1
alert(counters[1].get()); // возвращает 1
Обратите внимание, что counters[0]
и counters[1]
не являются независимыми. На самом деле, они работают с одним и тем же obj
!
Это происходит потому, что существует только одна копия obj
, общая для всех итераций цикла, возможно, из соображений производительности. Хотя {counter: 0}
создает новый объект в каждой итерации, одна и та же копия obj
будет просто обновляться ссылкой на самый новый объект.
Решение заключается в использовании вспомогательной функции:
function makeHelper(obj)
{
return {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = makeHelper(obj);
}
}
Это работает, потому что локальные переменные в области видимости функции, а также переменные-аргументы функции получают новые копии при входе в функцию.
Как перебрать или перечислить объекты в JavaScript?
Итерация через свойства объекта
Цикл по массиву в JavaScript
Как перебрать обычный объект JavaScript с объектами в качестве элементов?
Как создать диалог с кнопками "Ок" и "Отмена"