32

JavaScript замыкания внутри циклов — простой практический пример

15

Описание проблемы

Я столкнулся с проблемой при работе с замыканиями в 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 ответ(ов)

0

Другими словами, i в вашей функции связывается в момент выполнения функции, а не в момент её создания.

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

Большинство других ответов предлагают решения, основанные на создании другой переменной, значение которой не изменится.

Просто хотел добавить объяснение для ясности. Что касается решения, я бы лично выбрал подход Хартo, так как он является самым понятным способом из представленных здесь вариантов. Любой из приведённых кодов будет работать, но я предпочел бы фабрику замыканий, чем писать кучу комментариев, чтобы объяснить, почему я объявляю новую переменную (как у Фредди или из 1800-х) или использовать странный встроенный синтаксис замыкания (как у apphacker).

0

Вам нужно понять, что область видимости переменных в 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 будет хранить правильное значение для каждого созданного замыкания.

0

В этом примере рассматривается распространенная ошибка при использовании замыканий в 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);
  }
}

Это работает, потому что локальные переменные в области видимости функции, а также переменные-аргументы функции получают новые копии при входе в функцию.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь