Закрытие JavaScript внутри циклов - простой практический пример

2493

var funcs = [];
for (var i = 0; i < 3; i++) {      // let create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let run each one to see
}

Он выводит это:

Мое значение: 3
Мое значение: 3
Мое значение: 3

Если я хочу, чтобы он выводил:

Мое значение: 0
Мое значение: 1
Мое значение: 2


Та же проблема возникает, когда задержка при запуске функции вызвана использованием прослушивателей событий:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

... или асинхронный код, например, с помощью обещаний:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for(var i = 0; i < 3; i++){
  wait(i * 100).then(() => console.log(i)); // Log 'i' as soon as each promise resolves.
}

Каково решение этой основной проблемы?

  • 50
    Вы уверены, что не хотите, чтобы funcs был массивом, если вы используете числовые индексы? Просто один на один.
  • 22
    Это действительно запутанная проблема. Эта статья поможет мне понять это . Может ли это помочь другим?
Показать ещё 7 комментариев
Теги:
loops
closures

42 ответа

2004
Лучший ответ

Проблема в том, что переменная i в каждой из ваших анонимных функций связана с одной и той же переменной вне функции.

Классическое решение: затворы

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

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let run each one to see
}

Поскольку в JavaScript отсутствует область блока - только область функции - оборачивая создание функции в новую функцию, вы гарантируете, что значение "i" останется таким, как вы предполагали.


Решение 2015: forEach

Учитывая относительно широкую доступность функции Array.prototype.forEach (в 2015 году), стоит отметить, что в тех ситуациях, когда итерации .forEach() основном по массиву значений, .forEach() обеспечивает чистый, естественный способ получить четкое замыкание для каждая итерация То есть, если у вас есть какой-то массив, содержащий значения (ссылки DOM, объекты и т.д.), И возникает проблема настройки обратных вызовов, специфичных для каждого элемента, вы можете сделать это:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

Идея состоит в том, что каждый вызов функции обратного вызова, используемой с циклом .forEach будет собственным закрытием. Параметр, передаваемый этому обработчику, является элементом массива, специфичным для данного конкретного шага итерации. Если он используется в асинхронном обратном вызове, он не будет конфликтовать ни с одним из других обратных вызовов, установленных на других этапах итерации.

Если вы работаете в jQuery, $.each() дает вам аналогичную возможность.


Решение ES6: let

ECMAScript 6 (ES6) представляет новые ключевые слова let и const которые имеют область действия, отличную от переменных var -based. Например, в цикле с индексом let -based каждая итерация цикла будет иметь новое значение i где каждое значение находится в пределах цикла, поэтому ваш код будет работать так, как вы ожидаете. Ресурсов много, но я бы рекомендовал пост-обзор 2ality в качестве отличного источника информации.

for (let i = 0; i < 3; i++) {
    funcs[i] = function() {
        console.log("My value: " + i);
    };
}

Остерегайтесь, однако, того, что IE9-IE11 и Edge до поддержки Edge 14 let но ошибаются выше (они не создают новый i каждый раз, поэтому все функции выше будут регистрировать 3, как если бы мы использовали var). Край 14, наконец, понимает это правильно.

  • 1
    Это было хорошим объяснением, я читал через замыкания, но не совсем понял, как значение i не уникально как 1,2,3. Но после прочтения вашего поста, я понял, что javascript не имеет области видимости блока и имеет только область видимости функции. Спасибо за простой ответ.
  • 7
    не function createfunc(i) { return function() { console.log("My value: " + i); }; } все еще закрытие, потому что он использует переменную i ?
Показать ещё 22 комментария
353

Try:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Изменить (2014):

Лично я думаю, что @Aust более свежий ответ об использовании .bind - лучший способ сделать это сейчас. Там также lo-dash/underscore _.partial, когда вам не нужно или хотите испортить bind thisArg.

  • 1
    любое объяснение о }(i)); ?
  • 3
    @aswzen Я думаю, что он передает i в качестве index аргумента функции.
Показать ещё 1 комментарий
324

Еще один способ, который еще не упоминался, - использование Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

UPDATE

Как указано @squint и @mekdev, вы получаете лучшую производительность, сначала создавая функцию за пределами цикла, а затем привязывая результаты в цикле.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}
  • 0
    Это то, что я делаю в эти дни, мне также нравится _.partial lo-dash / underscore
  • 16
    .bind() будет в значительной степени устаревшим с функциями ECMAScript 6. Кроме того, это фактически создает две функции на одну итерацию. Сначала аноним, а затем сгенерированный .bind() . Лучше было бы создать его вне цикла, а затем .bind() внутри.
Показать ещё 4 комментария
247

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

for (var i = 0; i < 3; i++) {

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

Это отправляет итератор i в анонимную функцию, которую мы определяем как index. Это создает замыкание, в котором переменная i сохраняется для последующего использования в любых асинхронных функциях внутри IIFE.

  • 8
    Для большей читаемости кода и во избежание путаницы относительно того, что i есть, я бы переименовал параметр функции в index .
  • 4
    Как бы вы использовали эту технику для определения функций массива, описанных в исходном вопросе?
Показать ещё 8 комментариев
139

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

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

 //перезаписать console.log(), чтобы вы могли видеть вывод консоли
console.log = function (msg) {document.body.innerHTML + = '<p>' + msg + '</p>';};

var funcs = {};
для (var я = 0; я < 3; я ++) {
   var ilocal = i;//создаем новую локальную переменную
   funcs [i] = function() {
       console.log( "Мое значение:"  + +);//каждый должен ссылаться на свою локальную переменную
   };
}
для (var j = 0; j < 3; j ++) {
   funcs [J]();
}Код>

Так же, как и раньше, когда каждая внутренняя функция выдавала последнее значение, присвоенное i, теперь каждая внутренняя функция выводит последнее значение, присвоенное ilocal. Но не должна ли каждая итерация иметь собственный ilocal?

Оказывается, в этом и проблема. Каждая итерация разделяет одну и ту же область, поэтому каждая итерация после первого просто перезаписывает ilocal. Из MDN:

Важно: JavaScript не имеет области действия блока. Переменные, введенные с блоком, привязаны к содержащейся функции или скрипту, а эффекты их настройки сохраняются за пределами самого блока. Другими словами, операторы блоков не вводят область. Хотя "автономные" блоки являются допустимым синтаксисом, вы не хотите использовать автономные блоки в JavaScript, потому что они не делают то, что, по вашему мнению, они делают, если вы думаете, что они делают что-то вроде таких блоков на C или Java.

Повторяется для акцента:

JavaScript не имеет области действия блока. Переменные, введенные с блоком, привязаны к содержащейся функции или скрипту

Мы можем это увидеть, проверив ilocal, прежде чем объявить его на каждой итерации:

 //перезаписать console.log(), чтобы вы могли видеть вывод консоли
console.log = function (msg) {document.body.innerHTML + = '<p>' + msg + '</p>';};

var funcs = {};
для (var я = 0; я < 3; я ++) {
 console.log(ilocal);
 var ilocal = i;
}Код>

Именно поэтому эта ошибка настолько сложна. Несмотря на то, что вы обновляете переменную, Javascript не будет вызывать ошибку, и JSLint даже не выдаст предупреждение. Именно поэтому наилучшим способом решить эту проблему является использование замыканий, что по сути является идеей, что в Javascript внутренние функции имеют доступ к внешним переменным, потому что внутренние области "заключают" внешние области.

Изображение 2047

Это также означает, что внутренние функции "удерживают" внешние переменные и сохраняют их, даже если внешняя функция возвращается. Чтобы использовать это, мы создаем и вызываем функцию-обертку исключительно для создания новой области, объявляем ilocal в новой области и возвращаем внутреннюю функцию, которая использует ilocal (больше объяснений ниже):

 //перезаписать console.log(), чтобы вы могли видеть вывод консоли
console.log = function (msg) {document.body.innerHTML + = '<p>' + msg + '</p>';};

var funcs = {};
для (var я = 0; я < 3; я ++) {
   funcs [i] = (function() {//создаем новую область с помощью функции обертки
       var ilocal = i;//захватываем я в локальный var
       return function() {//возвращает внутреннюю функцию
           console.log( "Мое значение:"  + +);
       };
   })();//не забудьте запустить функцию обертки
}
для (var j = 0; j < 3; j ++) {
   funcs [J]();
}Код>

Создание внутренней функции внутри функции-оболочки дает внутренней функции частную среду, доступ к которой может получить только "закрытие". Таким образом, каждый раз, когда мы вызываем функцию-оболочку, мы создаем новую внутреннюю функцию с ее собственной отдельной средой, гарантируя, что переменные ilocal не сталкиваются и не перезаписывают друг друга. Несколько незначительных оптимизаций дают окончательный ответ, который дали многие другие пользователи SO:

 //перезаписать console.log(), чтобы вы могли видеть вывод консоли
console.log = function (msg) {document.body.innerHTML + = '<p>' + msg + '</p>';};

var funcs = {};
для (var я = 0; я < 3; я ++) {
   funcs [i] = обертка (i);
}
для (var j = 0; j < 3; j ++) {
   funcs [J]();
}
// создает отдельную среду для внутренней функции
обертка функций (илокальная) {
   return function() {//возвращает внутреннюю функцию
       console.log( "Мое значение:"  + +);
   };
}Код>

Обновление

Теперь, когда основной элемент ES6 теперь доступен, мы можем использовать новое ключевое слово let для создания переменных с блочным диапазоном:

 //перезаписать console.log(), чтобы вы могли видеть вывод консоли
console.log = function (msg) {document.body.innerHTML + = '<p>' + msg + '</p>';};

var funcs = {};
для (пусть я = 0; я < 3; я ++) {//использовать "let" для объявления "i" 
   funcs [i] = function() {
       console.log( "Мое значение:"  + i);//каждый должен ссылаться на свою локальную переменную
   };
}
for (var j = 0; j < 3; j ++) {//мы можем использовать здесь "var" без проблем
   funcs [J]();
}Код>

Посмотрите, как легко это сейчас! Для получения дополнительной информации см. этот ответ, на котором основана моя информация.

  • 0
    Мне нравится, как ты объяснил способ IIFE. Я искал это. Спасибо.
  • 3
    Теперь в JavaScript существует такая вещь, как определение области блоков с использованием ключевых слов let и const . Если бы этот ответ был расширен, чтобы включить его, на мой взгляд, он был бы гораздо более полезным во всем мире.
Показать ещё 4 комментария
135

Благодаря тому, что ES6 теперь широко поддерживается, лучший ответ на этот вопрос изменился. ES6 предоставляет ключевые слова let и const для этого точного обстоятельства. Вместо того, чтобы возиться с закрытием, мы можем просто использовать let чтобы установить переменную области цикла таким образом:

var funcs = [];
for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value: " + i); 
    };
}

val затем укажет на объект, специфичный для этого конкретного поворота цикла, и вернет правильное значение без дополнительной записи о замыкании. Это, очевидно, значительно упрощает эту проблему.

const аналогично let с дополнительным ограничением, что имя переменной не может быть отскок к новой ссылке после первоначального назначения.

Поддержка браузера теперь предназначена для тех, кто ориентируется на последние версии браузеров. const/let в настоящее время поддерживаются в последних версиях Firefox, Safari, Edge и Chrome. Он также поддерживается в узле, и вы можете использовать его в любом месте, используя инструменты построения, такие как Babel. Вы можете увидеть рабочий пример здесь: http://jsfiddle.net/ben336/rbU4t/2/

Документы здесь:

Остерегайтесь, однако, что поддержка IE9-IE11 и Edge до Edge 14 let но получить вышеизложенное (они не создают новый i каждый раз, поэтому все вышеперечисленные функции будут записывать 3, как если бы мы использовали var). Edge 14, наконец, все исправит.

  • 0
    К сожалению, «let» все еще не полностью поддерживается, особенно в мобильных устройствах. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
  • 2
    По состоянию на июнь 16 года, let поддерживается во всех основных версиях браузеров, кроме iOS Safari, Opera Mini и Safari 9. Это поддерживают браузеры Evergreen. Babel будет передавать его правильно, чтобы сохранить ожидаемое поведение без включения режима высокой совместимости.
Показать ещё 2 комментария
80

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

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

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

Просто подумал, что добавлю объяснение для ясности. Для решения, лично, я бы пошел с Harto, так как это самый понятный способ сделать это из ответов здесь. Любой из опубликованных кодов будет работать, но я бы выбрал закрытие factory из-за необходимости писать кучу комментариев, чтобы объяснить, почему я объявляю новую переменную (Freddy и 1800) или имеет странный встроенный синтаксис закрытия (apphacker).

63

Что вам нужно понять, так это объем переменных в javascript, основанный на функции. Это важная разница, чем сказать С#, где у вас есть область блока, и просто копирование переменной в одну из них будет работать.

Обертывание его в функции, которая оценивает возврат функции, как ответ apphacker, сделает трюк, так как переменная теперь имеет область действия.

Существует также ключевое слово let вместо var, которое позволит использовать правило области блока. В этом случае определение переменной внутри for сделает трюк. Тем не менее, ключевое слово let не является практичным решением из-за совместимости.

var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value: " + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}
  • 0
    @nickf какой браузер? как я уже сказал, у него есть проблемы с совместимостью, и я имею в виду серьезные проблемы с совместимостью, как будто я не думаю, что let поддерживается в IE.
  • 0
    ... ни Firefox. Мы говорим о JavaScript, верно?
Показать ещё 5 комментариев
53

Здесь другая вариация метода, аналогичная Bjorn (apphacker), которая позволяет вам назначать значение переменной внутри функции, а не передавать ее как параметр, который иногда может быть более ясным:

for (var i = 0; i < 3; i++) {
    funcs[i] = (function() {
        var index = i;
        return function() {
            console.log("My value: " + index);
        }
    })();
}

Обратите внимание, что любой метод, который вы используете, переменная index становится своего рода статической переменной, связанной с возвращенной копией внутренней функции. I.e., изменения его значения сохраняются между вызовами. Это может быть очень удобно.

  • 0
    Спасибо и ваше решение работает. Но я хотел бы спросить, почему это работает, но замена строки var и return не будет работать? Спасибо!
  • 0
    @midnite Если вы поменяете местами var и return переменная не будет назначена до того, как она return внутреннюю функцию.
49

Это описывает распространенную ошибку с использованием закрытий в 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()); // returns 1
alert(counter2.get()); // returns 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()); // returns 1
alert(counters[1].get()); // returns 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 для закрытия

43

Самое простое решение:

Вместо использования:

var funcs = [];
for(var i =0; i<3; i++){
    funcs[i] = function(){
        alert(i);
    }
}

for(var j =0; j<3; j++){
    funcs[j]();
}

который предупреждает "2", в 3 раза. Это связано с тем, что анонимные функции, созданные в цикле for, разделяют одно и то же закрытие, и при этом закрытие значение i совпадает. Используйте это, чтобы предотвратить совместное закрытие:

var funcs = [];
for(var new_i =0; new_i<3; new_i++){
    (function(i){
        funcs[i] = function(){
            alert(i);
        }
    })(new_i);
}

for(var j =0; j<3; j++){
    funcs[j]();
}

Идея этого заключается в том, что инкапсуляция всего тела цикла for с помощью IIFE (выражение с мгновенной вызывной функцией) и передача new_i как параметр и зафиксировать его как i. Поскольку анонимная функция выполняется немедленно, значение i отличается для каждой функции, определенной внутри анонимной функции.

Это решение, похоже, подходит для любой такой проблемы, так как оно потребует минимальных изменений исходного кода, страдающего этой проблемой. Фактически, это по дизайну, это не должно быть проблемой вообще!

  • 2
    Прочитайте нечто подобное в книге один раз. Я тоже предпочитаю это, поскольку вам не нужно прикасаться к существующему коду (так много), и становится очевидным, почему вы это сделали, как только вы выучите образец функции с самопризывом: перехватить эту переменную во вновь созданном объем.
  • 1
    @DanMan Спасибо. Самозвонящиеся анонимные функции - очень хороший способ справиться с отсутствием в javascript области видимости переменных уровня блока.
Показать ещё 1 комментарий
26

попробуйте этот более короткий

  • нет массива

  • нет дополнительных для цикла


for (var i = 0; i < 3; i++) {
    createfunc(i)();
}

function createfunc(i) {
    return function(){console.log("My value: " + i);};
}

http://jsfiddle.net/7P6EN/

  • 1
    Ваше решение, кажется, выводит корректно, но оно без необходимости использует функции, почему бы не просто console.log выводить? Первоначальный вопрос - о создании анонимных функций с таким же закрытием. Проблема заключалась в том, что, поскольку они имеют одно замыкание, значение i одинаково для каждого из них. Я надеюсь, что вы получили это.
21

Здесь простое решение, использующее forEach (работает с IE9):

var funcs = [];
[0,1,2].forEach(function(i) {          // let create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
})
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let run each one to see
}

Печать:

My value: 0
My value: 1
My value: 2
  • 0
    forEach не поддерживает IE8 или более низкую версию !!!
  • 0
    Вот почему я сказал "работает обратно в IE9" ....
21

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

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

Ошибка на самом деле не возникает, пока funcs[someIndex] не выполняется (). Используя эту же логику, должно быть очевидно, что значение i также не будет собрано до этой точки. Как только исходный цикл заканчивается, i++ добавляет i к значению 3, что приводит к неудаче условия i < 3 и завершению цикла. На этом этапе i равен 3, поэтому, когда используется funcs[someIndex](), и i оценивается, он равен 3 - каждый раз.

Чтобы пройти мимо этого, вы должны оценить i по мере его возникновения. Обратите внимание, что это уже произошло в форме funcs[i] (где есть 3 уникальных индекса). Существует несколько способов захвата этого значения. Один из них - передать его в качестве параметра функции, которая уже несколько раз показана здесь.

Другой вариант - построить объект функции, который сможет закрыть эту переменную. Это может быть выполнено таким образом

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};
18

Функции JavaScript "закрывают" область, к которой они имеют доступ при объявлении, и сохраняют доступ к этой области, даже если переменные в этой области изменяются.

var funcs = []

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

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

В дальнейшем эти функции вызывают регистрацию самого текущего значения i в глобальной области. Это волшебство и расстройство закрытия.

"Функции JavaScript закрываются по области, в которой они объявлены, и сохраняют доступ к этой области, даже если переменные значения внутри этой области изменяются".

Использование let вместо var решает это, создавая новую область при каждом запуске цикла for, создавая выделенную область для закрытия каждой функции. Различные другие методы делают то же самое с дополнительными функциями.

var funcs = []

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

for (var k = 0; k < 3; k += 1) {
  funcs[k]()
}

(let делает переменные областью с областью действия, а не функцией. Блоки обозначаются фигурными фигурными скобками, но в случае цикла for переменная инициализации i в нашем случае считается объявленной в скобки.)

  • 1
    Я изо всех сил пытался понять эту концепцию, пока не прочитал этот ответ. Это затрагивает действительно важный момент - значение i устанавливается в глобальном масштабе. Когда цикл for завершает работу, глобальное значение i теперь равно 3. Следовательно, всякий раз, когда эта функция вызывается в массиве (используя, скажем, funcs[j] ), i в этой функции ссылается на глобальную переменную i (которая является 3).
13

Прочитав различные решения, я хотел бы добавить, что причина, по которой эти решения работают, заключается в том, чтобы опираться на концепцию цепочки областей видимости. Это способ, которым JavaScript разрешает переменную во время выполнения.

  • Каждое определение функции формирует область, состоящую из всех локальных переменных, объявленных var и ее arguments.
  • Если у нас есть внутренняя функция, определенная внутри другой (внешней) функции, это образует цепочку и будет использоваться во время выполнения
  • Когда функция выполняется, среда выполнения оценивает переменные путем поиска в цепочке областей видимости. Если переменная может быть найдена в определенной точке цепочки, она перестанет искать и использовать ее, иначе она будет продолжаться до тех пор, пока глобальная область не достигнет того, что принадлежит window.

В исходном коде:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

Когда funcs выполняется, цепочка областей действия будет function inner → global. Поскольку переменная i не может быть найдена в function inner (не объявлена с использованием var или не передана как аргументы), она продолжает поиск, пока значение i будет найдено в глобальной области, которая является window.i.

Объединив его во внешнюю функцию, либо явно определите вспомогательную функцию, как harto, либо используйте анонимную функцию, такую как Bjorn:

funcs = {};
function outer(i) {              // function outer scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

Когда funcs выполняется, теперь цепочка области видимости будет function inner → function outer. На этот раз i могу найти в области внешней функции, которая выполняется 3 раза в цикле for, каждый раз имеет значение i связанное правильно. Он не будет использовать значение window.i когда выполняется внутреннее.

Более подробную информацию можно найти здесь
Это включает в себя распространенную ошибку при создании замыкания в цикле, как то, что у нас есть, а также почему нам нужно закрытие и рассмотрение производительности.

  • 0
    Мы редко пишем этот пример кода на практике, но я думаю, что он служит хорошим примером для понимания основ. После того, как мы Array.prototype.forEach(function callback(el) {}) область видимости и то, как они связаны друг с другом, становится яснее понять, почему другие «современные» способы, такие как Array.prototype.forEach(function callback(el) {}) естественным образом работают: обратный вызов, который передается, естественным образом формирует упаковка области с el правильно связаны в каждой итерации forEach . Таким образом, каждая внутренняя функция, определенная в обратном вызове, сможет использовать правильное значение el
12

С новыми функциями управления уровнем блока ES6 управляется:

var funcs = [];
for (let i = 0; i < 3; i++) {          // let create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (let j = 0; j < 3; j++) {
    funcs[j]();                        // and now let run each one to see
}

Код в вопросе OP заменяется на let вместо var.

  • 0
    const обеспечивает тот же результат, и его следует использовать, когда значение переменной не изменится. Однако использование const внутри инициализатора цикла for неправильно реализовано в Firefox и до сих пор не исправлено. Вместо того, чтобы быть объявленным внутри блока, он объявляется вне блока, что приводит к повторному объявлению переменной, что, в свою очередь, приводит к ошибке. Использование let в инициализаторе правильно реализовано в Firefox, поэтому не нужно беспокоиться.
8

Этот вопрос действительно показывает историю JavaScript! Теперь мы можем избежать блочного охвата функциями стрелок и обрабатывать петли непосредственно из узлов DOM с помощью методов Object.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>
8

Я удивлен, что никто еще не предложил использовать функцию forEach, чтобы лучше избегать (повторного) использования локальных переменных. Фактически, я больше не использую for(var i ...) по этой причине.

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

//отредактирован для использования forEach вместо карты.

  • 3
    .forEach() - намного лучший вариант, если вы на самом деле ничего не отображаете, и Дэрил предложил сделать это за 7 месяцев до публикации, так что нечего удивляться.
  • 0
    Этот вопрос не о цикле над массивом
Показать ещё 1 комментарий
7

Мы будем проверять, что на самом деле происходит, когда вы заявляете, var и let один за другим.

Случай1: использование var

<script>
   var funcs = [];
   for (var i = 0; i < 3; i++) {
     funcs[i] = function () {
        debugger;
        console.log("My value: " + i);
     };
   }
   console.log(funcs);
</script>

Теперь откройте окно консоли Chrome, нажав F12 и обновите страницу. Расходуйте каждые 3 функции внутри массива. Вы увидите свойство, называемое [[Scopes]] Разместите это. Вы увидите один объект массива под названием "Global", разверните его. Вы найдете свойство 'i' объявленное в объект, имеющий значение 3.

Изображение 2048

Изображение 2049

Вывод:

  1. Когда вы объявляете переменную с помощью 'var' вне функции, она становится глобальной переменной (вы можете проверить, введя i или window.i в window.i консоли. Он вернется 3).
  2. Анонимная функция, которую вы объявили, не будет вызывать и проверять значение внутри функции, если вы не вызываете функции.
  3. Когда вы вызываете эту функцию, console.log("My value: " + i) принимает значение из своего объекта Global и отображает результат.

CASE2: использование let

Теперь замените 'var' на 'let'

<script>
    var funcs = [];
    for (let i = 0; i < 3; i++) {
        funcs[i] = function () {
           debugger;
           console.log("My value: " + i);
        };
    }
    console.log(funcs);
</script>

Сделайте то же самое, перейдите в области. Теперь вы увидите два объекта "Block" и "Global". Теперь раскройте Block объекта, вы увидите "я" определяется там, и странное дело в том, что для каждой функции, значение, если i разная (0, 1, 2).

Изображение 2050

Вывод:

Когда вы объявляете переменную, используя 'let' даже вне функции, но внутри цикла, эта переменная не будет глобальной переменной, она станет переменной уровня Block доступной только для одной и той же функции. Вот почему мы получение значения i для каждой функции при вызове функций.

Для более подробной информации о том, как работает более близко, пройдите через потрясающий видеоурок https://youtu.be/71AtaJpJHw0

7

Прежде всего, поймите, что не так с этим кодом:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let run each one to see
}

Здесь, когда инициализируется массив funcs[], увеличивается i, массив funcs инициализируется, а размер массива func становится равным 3, поэтому i = 3,. Теперь, когда вызывается funcs[j](), он снова использует переменную i, которая уже была увеличена до 3.

Теперь, чтобы решить это, у нас есть много вариантов. Ниже приведены два из них:

  • Мы можем инициализировать i с помощью let или инициализировать новую переменную index с помощью let и сделать ее равной i. Поэтому, когда вызов выполняется, index будет использоваться, и его область действия закончится после инициализации. И для вызова снова будет инициализирован index:

    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]();                        
    }
    
  • Другой вариант - ввести tempFunc, который возвращает действительную функцию:

    var funcs = [];
    function tempFunc(i){
        return function(){
            console.log("My value: " + i);
        };
    }
    for (var i = 0; i < 3; i++) {  
        funcs[i] = tempFunc(i);                                     
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
    
5

Используйте closure, это уменьшит ваш дополнительный цикл. Вы можете сделать это за один цикл:

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}
5

Причина, по которой ваш исходный пример не работает, заключается в том, что все замыкания, созданные в цикле, ссылаются на один и тот же фрейм. По сути, имеет 3 метода на одном объекте только с одной переменной i. Все они напечатали одинаковое значение.

4

Используйте ключевое слово ECMA-6 let, чтобы связать область видимости с блоком

var funcs = [];
for (let i = 0; i < 3; i++) {      // let create 3 functions
funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
 };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let run each one to see
} 
3
var funcs = [];
for (var i = 0; i < 3; i++) {      // let create 3 functions
  funcs[i] = function(param) {          // and store them in funcs
    console.log("My value: " + param); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j](j);                      // and now let run each one to see with j
}
3

Я предпочитаю использовать функцию forEach, которая имеет собственное закрытие с созданием псевдодиапазона:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

for (var j = 0; j < 3; j++) {
    funcs[j](); // 0, 1, 2
}

Это выглядит уродливее, чем диапазоны на других языках, но IMHO менее чудовищно, чем другие решения.

  • 0
    Предпочитаете это чем? Это, кажется, комментарий в ответ на какой-то другой ответ. Он вообще не затрагивает фактический вопрос (поскольку вы не назначаете функцию, которая будет вызываться позже, где угодно).
  • 0
    Теперь понятно?
Показать ещё 4 комментария
2

Многие решения кажутся правильными, но они не упоминают его как Currying, который является шаблоном проектирования функционального программирования для ситуаций, подобных здесь. 3-10 раз быстрее, чем привязка в зависимости от браузера.

var funcs = [];
for (var i = 0; i < 3; i++) {      // let create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let run each one to see
}

function curryShowValue(i) {
  return function showValue() {
    console.log("My value: " + i);
  }
}

Смотрите коэффициент производительности в разных браузерах.

  • 0
    @TinyGiant Пример с возвращаемой функцией все еще оптимизирован для производительности. Я бы не стал использовать функции стрелок, как все блоггеры JavaScript. Они выглядят круто и чисто, но способствуют написанию встроенных функций вместо использования предопределенных функций. Это может быть неочевидной ловушкой в жарких местах. Другая проблема заключается в том, что они не являются просто синтаксическим сахаром, потому что они выполняют ненужные привязки, создавая таким образом замыкания обертки.
  • 1
    Предупреждение для будущих читателей: в этом ответе неточно используется термин карринг . «Каррирование - это когда вы разбиваете функцию, которая принимает несколько аргументов, на ряд функций, которые принимают часть аргументов». , Этот код не делает ничего подобного. Все, что вы здесь сделали, - это взяли код из принятого ответа, изменили некоторые вещи, изменили стиль и немного назвали, а затем назовите его каррированием, что категорически не так.
2

Ваш код не работает, потому что он делает это:

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

Теперь возникает вопрос: каково значение переменной i при вызове функции? Поскольку первый цикл создается с условием i < 3, он немедленно останавливается, когда условие ложно, поэтому оно i = 3.

Вам нужно понять, что во время создания ваших функций ни один из их кодов не выполняется, он сохраняется только позже. Поэтому, когда их вызывают позже, интерпретатор выполняет их и спрашивает: "Каково текущее значение i?"

Итак, ваша цель - сначала сохранить значение i для функции, и только после этого сохраните функцию до funcs. Это можно сделать, например, следующим образом:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let create 3 functions
    funcs[i] = function(x) {            // and store them in funcs
        console.log("My value: " + x); // each should log its value.
    }.bind(null, i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let run each one to see
}

Таким образом, каждая функция будет иметь свою собственную переменную x, и мы устанавливаем значение x на значение i на каждой итерации.

Это только один из нескольких способов решения этой проблемы.

2

И еще одно решение: вместо создания другого цикла просто привяжите this к функции возврата.

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

Связывая это, также решает проблему.

2

Вы можете использовать декларативный модуль для списков данных, таких как query-js (*). В этих ситуациях я лично считаю декларативный подход менее неожиданным.

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

Затем вы можете использовать свой второй цикл и получить ожидаемый результат, или вы могли бы сделать

funcs.iterate(function(f){ f(); });

(*) Я автор запросов-js и поэтому предвзятый к его использованию, поэтому не принимайте мои слова в качестве рекомендации для указанной библиотеки только для декларативного подхода:)

  • 1
    Я бы с удовольствием объяснил это. Код решает проблему под рукой. Было бы полезно узнать, как потенциально улучшить код
  • 1
    Что такое Query.range(0,3) ? Это не является частью тегов для этого вопроса. Кроме того, если вы используете стороннюю библиотеку, вы можете предоставить ссылку на документацию.
Показать ещё 1 комментарий
1

Просто измените ключевое слово var.

var - это область действия.

пусть это блочный охват.

Когда вы запускаете код, цикл for будет итерировать и присваивать значение я равному 3, которое останется 3 на вашем коде. Я предлагаю вам больше узнать о облаках в узле (let, var, const и другие)

funcs = [];
for (let i = 0; i < 3; i++) {      // let create 3 functions
  funcs[i] =async function() {          // and store them in funcs
    await console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let run each one to see
}
1

Это доказывает, насколько уродливый javascript относится к тому, как работает "закрытие" и "не-закрытие".

В случае:

var funcs = [];

for (var i = 0; i < 3; i++) {      // let create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}

funcs [i] - глобальная функция и "console.log(" My value: "+ i); печатает глобальную переменную i

В случае

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

из-за этой скрученной конструкции закрытия javascript, "console.log(" My value: "+ i); печатает я из внешней функции 'createfunc (i)'

все потому, что javascript не может создать что-то приличное, как "статическая" переменная внутри функции, как то, что делает язык программирования C!

1

СЧЕТЧИК ПРЯМОЙ

Определить функции обратного вызова следующим образом:

// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
    for (var i=0; i<2; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
}
test1();
// 2
// 2

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

Чтобы передать и сохранить значение при определении обратного вызова, мы можем создать closure, чтобы сохранить значение до того, как обратный вызов будет вызывается. Это можно сделать следующим образом:

function test2() {
    function sendRequest(i) {
        setTimeout(function() {
            console.log(i);
        });
    }

    for (var i = 0; i < 2; i++) {
        sendRequest(i);
    }
}
test2();
// 1
// 2

Теперь, что особенно важно, это "Примитивы передаются по значению и копируются. Таким образом, когда ограничение определено, они сохраняют значение из предыдущего цикла".

COUNTER THE OBJECT

Так как у закрытий есть доступ к родительским переменным функции через ссылку, этот подход будет отличаться от такового для примитивов.

// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
    var index = { i: 0 };
    for (index.i=0; index.i<2; index.i++) {
        setTimeout(function() {
            console.log('test3: ' + index.i);
        });
    }
}
test3();
// 2
// 2

Таким образом, даже если для переменной, передаваемой как объект, создается замыкание, значение индекса цикла не будет сохранено. Это означает, что значения объекта не копируются, а к ним обращаются через ссылку.

function test4() {
    var index = { i: 0 };
    function sendRequest(index, i) {
        setTimeout(function() {
            console.log('index: ' + index);
            console.log('i: ' + i);
            console.log(index[i]);
        });
    }

    for (index.i=0; index.i<2; index.i++) {
        sendRequest(index, index.i);
    }
}
test4();
// index: { i: 2}
// 0
// undefined

// index: { i: 2}
// 1
// undefined
1

Это проблема, с которой часто встречается асинхронный код, переменная i является изменяемой и в момент, когда происходит вызов функции, будет выполняться код с использованием i, а i будет изменен до последнего значение, что означает, что все функции, созданные в цикле, создадут closure и i будут равны 3 (верхняя граница + 1 цикла for.

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

0
for (var i = 1; i < 5; i++) {
console.log.call(true,i);
}
0

Почему бы просто не вызывать каждую функцию внутри первого (и только) цикла сразу после их создания, например:

 var funcs = [];
    for (var i = 0; i < 3; i++) {
    // let create 3 functions
    funcs[i] = function() {
    // and store them in funcs
    console.log("My value: " + i); // each should log its value.
    };
    funcs[i]();// and now let run each one to see
    }
  • 2
    Потому что это был просто пример проблемы.
0

Ok. Я прочитал все ответы. Хотя здесь есть хорошее объяснение - я просто не мог заставить это работать. Поэтому я пошел смотреть в Интернете. У человека в https://dzone.com/articles/why-does-javascript-loop-only-use-last-value был ответ, который здесь не представлен. Поэтому я подумал, что отправлю короткий пример. Это сделало для меня намного больше смысла.

Длинным и коротким является то, что команда LET хороша, но только сейчас используется. ОДНАКО, команда LET - это просто комманда TRY-CATCH. Это все возвращается к IE3 (я считаю). Использование компиляции TRY-CATCH прост и хорош. Вероятно, почему люди EMCScript решили использовать его. Он также не нуждается в функции setTimeout(). Таким образом, время не теряется. В принципе, для каждого цикла FOR нужен один TRY-CATCH. Вот пример:

    for( var i in myArray ){
       try{ throw i }
       catch(ii){
// Do whatever it is you want to do with ii
          }
       }

Если у вас более одного цикла FOR, вы просто поместите компилятор TRY-CATCH для каждого из них. Кроме того, лично я всегда использую двойную букву любой переменной FOR, которую я использую. Итак, "ii" для "i" и так далее. Я использую этот метод в рутине для отправки команд мыши на другую процедуру.

0

Скажем, вы не используете es6; Вы можете использовать функцию IFFY:

var funcs = [];
for (var i = 0; i < 13; i++) {      
funcs[i] = (function(x) {
console.log("My value: " + i)})(i);}

Но это будет по-другому.

0

Пока этот вопрос старый и ответил, у меня есть еще одно довольно интересное решение:

var funcs = [];

for (var i = 0; i < 3; i++) {     
  funcs[i] = function() {          
    console.log("My value: " + i); 
 };
}

for (var i = 0; i < 3; i++) {
  funcs[i]();
}

Изменение настолько мало, что почти трудно понять, что я сделал. Я переключил второй итератор с j на i. Это как-то обновляет состояние я вовремя, чтобы дать вам желаемый результат. Я сделал это случайно, но это имеет смысл, учитывая предыдущие ответы.

Я написал это, чтобы указать на это небольшое, но очень важное различие. Надеюсь, что это поможет прояснить некоторую путаницу для других учеников, таких как я.

Примечание. Я не разделяю это, потому что я думаю, что это правильный ответ. Это ошибочное решение, которое, вероятно, сломается при определенных обстоятельствах. На самом деле, я очень удивлен, что он действительно работает.

  • 0
    Это работает только потому, что во втором цикле вы перезаписываете то же значение i на которое ссылается функция. Просто учтите, что во всем этом фрагменте есть только одна переменная i . Это эквивалентно: i = 0; funcs[0](); i = 1; funcs[1](); ..
  • 0
    правильно, что имеет смысл, учитывая другие ответы о сфере охвата, но все еще является своего рода нелогичным
-1

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

let arr = [1,2,3];

let myFunc = (val, index) => { console.log('val: '+val+'\nindex: '+index); };

arr.forEach(myFunc);

  • 0
    forEach покрывается принятым ответом.
-2

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

Другое решение: используйте свой код так, как есть. Просто используйте переменную "i" во втором цикле поскольку вся созданная функция указывает на одно и то же значение "i".

var q_v = [];

for( var i = 0; i < 3; i++ ){
    q_v[i] = function(){
        console.log('Value of i : '+ i);
    };
}

console.log( i ); // the value of i = 3

for( var i = 0; i < 3; i++ ){ // update value of i
    q_v[i](); // on every loop, it will get the new value of 'i'
}
-2

Воспользуемся new Function. Таким образом, i перестает быть переменной closure и становится только частью текста:

var funcs = [];
for (var i = 0; i < 3; i++) {
    var functionBody = 'console.log("My value: ' + i + '");';
    funcs[i] = new Function(functionBody);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}
  • 3
    Это медленно, потенциально небезопасно и не работает везде.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню