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

avatar
nickf
15 апреля 2009 в 06:06
423889
44
3016

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

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

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

В то время как я хотел бы выводить:

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


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

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<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++) {
  // Log `i` as soon as each promise resolves.
  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(var f of fns){
  f();
}

Как решить эту основную проблему?

Источник
DanMan
26 июля 2013 в 11:12
62

Вы уверены, что не хотите, чтобы funcs был массивом, если вы используете числовые индексы? Просто предупреждаю.

user3199690
3 мая 2014 в 15:38
30

Это действительно сбивающая с толку проблема. Эта статья помогла мне понять это. Может, это поможет и другим.

Peter Krauss
17 декабря 2014 в 01:22
4

Другое простое и понятное решение: 1) Вложенные функции имеют доступ к области «над» ними; 2) a закрытие решение ... «Закрытие - это функция, имеющая доступ к родительской области, даже после закрытия родительской функции».

Saurabh Ahuja
2 апреля 2015 в 12:01
2

Обратитесь к этой ссылке для лучшего понимания javascript.info/tutorial/advanced-functions

Tomas Nikodym
13 сентября 2016 в 20:34
48

В ES6 тривиальным решением является объявление переменной i с помощью let, которая ограничена до тела цикла.

Costa Michailidis
7 декабря 2016 в 17:27
1

Функции JS «закрывают» область видимости, к которой они имеют доступ после объявления, и сохраняют доступ к этой области даже при изменении переменных в этой области. Каждая функция в приведенном выше массиве закрывается в глобальной области (глобальной просто потому, что это область, в которой они объявлены). Позже эти функции вызываются, регистрируя самое последнее значение i в глобальной области. Это JS :) let вместо var решает эту проблему, создавая новую область видимости при каждом запуске цикла, создавая отдельную область видимости для каждой функции, которую нужно закрыть. Различные другие методы делают то же самое с дополнительными функциями.

Costa Michailidis
7 декабря 2016 в 19:48
1

Ниже дан более подробный ответ: coderhelper.com/a/41023816/1027004

Pawel
28 февраля 2017 в 15:10
0

Шаблон проектирования идеально соответствует этому сценарию coderhelper.com/a/42512295/696535

MukulSharma
16 июня 2017 в 09:20
0

почему это не работает - funcs [i] = function abc () {return function () {console.log ("My value:" + i); };

S.D.
2 июня 2020 в 16:30
0

Добавлен пример этой проблемы в циклы for in и for of.

3limin4t0r
21 июля 2020 в 14:10
0

@ S.D. Я бы посоветовал хотя бы использовать ключевое слово var для добавленного примера. Использование глобальных переменных считается плохой практикой. for (var v of arr) (Для решения проблемы используйте let или const вместо var.)

S.D.
29 июля 2020 в 09:38
0

@ 3limin4t0r Поправил.

BKSpurgeon
7 сентября 2020 в 23:33
2

Вот почему я ненавижу JavaScript.

Ответы (44)

avatar
harto
15 апреля 2009 в 06:18
2285

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

Решение ES6: let

ECMAScript 6 (ES6) вводит новые ключевые слова let и const, область действия которых отличается от области действия переменных на основе var. Например, в цикле с индексом на основе let каждая итерация цикла будет иметь новую переменную 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). Edge 14 наконец-то понял это правильно.


Решение ES5.1: для каждого

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

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

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

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


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

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

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++) {
  // and now let's run each one to see
  funcs[j]();
}

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

PCA
17 августа 2013 в 13:08
2

Это было хорошее объяснение, я читал замыкания, но не совсем понял, почему значение i не уникально, как 1,2,3. Но после прочтения вашего сообщения я понял, что javascript не имеет области действия блока, а имеет только область действия. Спасибо за простой ответ.

アレックス
28 марта 2014 в 03:45
10

не является ли function createfunc(i) { return function() { console.log("My value: " + i); }; } закрытым, потому что он использует переменную i?

cookie monster
4 апреля 2014 в 19:30
0

@Alex: Да. Все функции JavaScript создают замыкания. Единственный вопрос - это состояние закрытой переменной, на которую ссылается функция. Решение создает новую область видимости переменной при вызове createfunc, которая имеет как значение i во время вызова, так и функцию, которая ссылается на этот i. Таким образом, каждая возвращаемая функция (закрытие) находится в своей собственной области действия, закрывающей свой собственный i.

Wladimir Palant
20 июня 2014 в 12:21
62

К сожалению, этот ответ устарел, и никто не увидит правильный ответ внизу - использование Function.bind() определенно предпочтительнее, см. Coderhelper.com/a/19323214/785541.

cookie monster
12 июля 2014 в 02:35
92

@Wladimir: Ваше предположение, что .bind() - это "правильный ответ" , неверно. У каждого свое место. С .bind() вы не можете связать аргументы без привязки значения this. Также вы получаете копию аргумента i без возможности изменять его между вызовами, что иногда необходимо. Так что это совершенно разные конструкции, не говоря уже о том, что реализации .bind() исторически были медленными. Конечно, в простом примере любой из них будет работать, но замыкания - важная концепция, которую нужно понять, и именно об этом и был вопрос.

greenoldman
21 ноября 2014 в 07:14
0

@ LoïcFaure-Lacroix, как let помогает в захвате значений, а не ссылок?

Christian Landgren
7 февраля 2015 в 10:23
11

Пожалуйста, прекратите использовать эти уловки функции for-return, используйте вместо них [] .forEach или [] .map, потому что они избегают повторного использования тех же переменных области видимости.

user1106925
29 июня 2015 в 16:31
42

@ChristianLandgren: Это полезно, только если вы повторяете массив. Эти методы не являются «взломами». Это важные знания.

Carsten Farving
22 сентября 2015 в 06:09
0

@ChristianLandgren Например, в асинхронных обратных вызовах вам может потребоваться использовать эти методы, потому что у вас нет массива для итерации. NirmitSrivastava В основном то, что работало в первые годы javascript, по-прежнему соответствует тому, что используется сегодня.

Christian Landgren
22 сентября 2015 в 07:15
0

@CarstenFarving При работе с асинхронными обратными вызовами важно, чтобы вы сохраняли ссылку на переменную с ограниченной областью видимости, поскольку родительское значение может быть изменено с момента создания запроса до момента получения ответа. Если у вас нет массива, просто создайте его с помощью [1,2,3] .forEach ((i) => doSomething (i))

arnabkaycee
26 ноября 2015 в 10:12
0

Это основная проблема, с которой сталкиваются переменные 'var', т.е. они имеют область видимости функции, а не блока. Переменные var подвергаются подъему. Эта проблема решена с помощью оператора let, представленного в ES6. Видеть. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…

JSG
18 апреля 2016 в 20:16
1

Мне кажется, что метод «let» отлично работает, так как он работает именно так, как я предполагал, что скрипт должен работать, прежде чем я наткнулся на проблемы с закрытием. Мне нужно было создать функцию на ходу, восстанавливая предыдущую функциональность для поведения onclick. В родительской функции переменные устанавливаются, вычисляются и передаются.

Costa Michailidis
7 декабря 2016 в 17:03
0

На самом деле это не так уж сложно. Соответствующий вопрос от @ ア レ ッ ク ス: «Разве функция createfunc (i) {return function () {console.log (" My value: "+ i);};} все еще закрыта, потому что она использует переменную i?» Ответ: да, конечно! Теперь вы это понимаете. Когда функция объявляется, она закрывается за пределы области, в которой она была объявлена. createFunc (i) - это совершенно новое закрытие, на один уровень глубже. Помните, как функции используют ссылку на ближайшую переменную? Что немного сбивает с толку, так это то, что мы использовали i в качестве аргумента для createFunc вместо n или какого-либо другого случайного счетчика.

nnnnnn
12 декабря 2016 в 03:01
3

Обратите внимание, что использование let внутри for цикла () не работает должным образом в IE (одно решение: добавьте переменную let внутри блока {} из for, что выполняет нормально работают в IE).

Somnium
13 февраля 2017 в 12:11
0

Использование let - лучший подход в новых браузерах, однако правильнее добавить let внутри for тела, как предлагает @nnnnnn, потому что заголовок цикла for имеет свою собственную область действия (однако он работает в не-IE браузеры по какой-то причине, когда этого не должно быть).

nnnnnn
13 февраля 2017 в 14:10
1

@Somnium - это IE неправильно его реализует. Остальные браузеры делают то, что указано в спецификации.

Pawel
27 февраля 2017 в 16:36
0

@WladimirPalant ответ, на который вы ссылаетесь, - худшее решение, чем это. "bind" подходит для setTimeout и обратных вызовов, но не для использования в цикле.

Wladimir Palant
27 февраля 2017 в 17:23
0

@Pawel: Этот вопрос касается создания экземпляров конкретной функции, привязанной к определенным параметрам - это именно то, для чего подходит Function.bind(). Цикл просто демонстрирует принцип, он просто связывает переменные, которые могут измениться постфактум. В реальном цикле вы, конечно, уже могли бы использовать оператор let, но, к сожалению, его поддержка только сейчас становится достаточной для использования.

Pawel
28 февраля 2017 в 15:01
0

@WladimirPalant bind основная функция - привязать контекст, а не атрибуты. Атрибуты добавляются к контексту, поэтому bind в этом случае является излишним. По сравнению с возвратом функции привязка в 3 раза медленнее в Chrome и в 10 раз медленнее в Firefox и Edge.

JLRishe
4 апреля 2018 в 20:20
1

@ mbomb007 Нет, это не так. Попробуйте запустить следующее в IE, Firefox и Chrome. Вы получите разные результаты: (function () { for(let a = 0; a < 10; a += 1) { setTimeout(function() { console.log(a); }, 100); } })()

mbomb007
4 апреля 2018 в 21:27
0

@JLRishe Кто-то должен сказать caniuse.com добавить примечание №4 в IE11, а затем

JLRishe
5 апреля 2018 в 07:22
1

@ mbomb007 Похоже, это немного отличается от №4, но я отправил PR, чтобы попытаться исправить свои данные.

Ashish Kamble
18 сентября 2020 в 11:09
0

@harto что это значит by wrapping the function creation in a new function, you ensure that the value of "i" remains as you intended ??

Noman_ibrahim
16 ноября 2020 в 20:52
0

Я заменил свой var на let, и он отлично работает. Я пытался перебирать элементы в транспортире. Использование var дало мне undefined. Но поскольку я использовал let вместо var, теперь я получаю реальное значение

himanshu sharma
18 января 2021 в 10:58
0

По вопросам собеседования, связанных с закрытием, загляните в этот замечательный блог idontknowjavascript.com/2019/05/…

avatar
CertainPerformance
21 сентября 2019 в 21:34
4

Если у вас такая проблема с циклом while, а не с циклом for, например:

var i = 0;
while (i < 5) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
  i++;
}

Техника закрытия при превышении текущего значения немного отличается. Объявите переменную области блока с const внутри блока while и назначьте ей текущий i. Затем, где бы переменная ни использовалась асинхронно, замените i новой переменной с блочной областью видимости:

var i = 0;
while (i < 5) {
  const thisIterationI = i;
  setTimeout(function() {
    console.log(thisIterationI);
  }, i * 1000);
  i++;
}

Для старых браузеров, которые не поддерживают переменные с блочной областью видимости, вы можете использовать IIFE, вызываемый с i:

var i = 0;
while (i < 5) {
  (function(innerI) {
    setTimeout(function() {
      console.log(innerI);
    }, innerI * 1000);
  })(i);
  i++;
}

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

var i = 0;
while (i < 5) {
  setTimeout(
    (thisIterationI) => { // Callback
      console.log(thisIterationI);
    },
    i * 1000, // Delay
    i // Gets passed to the callback; becomes thisIterationI
  );
  i++;
}
3limin4t0r
22 сентября 2019 в 20:06
0

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

Noman_1
12 февраля 2021 в 10:12
0

IIFE - это то, что я искал

avatar
Shivang Gupta
2 мая 2019 в 11:57
4

До ES5, эта проблема может быть решена только с помощью закрытия .

Но теперь в ES6 у нас есть переменные области видимости блока. Изменение var на let в первом цикле for решит проблему.

var funcs = [];
for (let i = 0; i < 3; i++) {      // let's 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's run each one to see
}
avatar
Murtaza Hussain
17 апреля 2019 в 08:10
-1

При поддержке ES6 лучший способ добиться этого - использовать ключевые слова let и const именно для этого случая. Итак, переменная var получает hoisted, и с концом цикла значение i обновляется для всех closures ... мы можем просто использовать let, чтобы установить переменную области цикла следующим образом:

var funcs = [];
for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value: " + i); 
    };
}
avatar
B G Hari Prasad
15 апреля 2019 в 10:37
3

Используйте let (blocked-scope) вместо var.

var funcs = [];
for (let i = 0; i < 3; i++) {      
  funcs[i] = function() {          
    console.log("My value: " + i); 
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      
}
avatar
swogger
7 апреля 2019 в 16:45
0
  asyncIterable = [1,2,3,4,5,6,7,8];

  (async function() {
       for await (let num of asyncIterable) {
         console.log(num);
       }
    })();
Federico Grandi
7 апреля 2019 в 18:55
0

Всегда лучше добавлять пояснения к коду, чтобы пользователи лучше его понимали. Попробуйте объяснить, почему ваш способ лучше, чем из других ответов :)

swogger
8 апреля 2019 в 23:01
0

Спасибо @FedericoGrandi, после двух страниц примеров я подумал, что это не понадобится.

avatar
Nouman Dilshad
5 сентября 2018 в 13:00
1

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

var - это функция в области видимости.

пусть имеет область действия блока.

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

funcs = [];
for (let i = 0; i < 3; i++) {      // let's 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's run each one to see
}
avatar
user1559625
1 августа 2018 в 09:32
0

Это доказывает, насколько уродливым является javascript в отношении того, как работают «закрытие» и «не закрытие».

В случае:

var funcs = [];

for (var i = 0; i < 3; i++) {      // let's 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 ("Мое значение:" + i);' печатает i из внешней функции createfunc (i)

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

avatar
Ashish Yadav
13 июля 2018 в 08:02
3
var funcs = [];
for (var i = 0; i < 3; i++) {      // let's 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's run each one to see with j
}
avatar
stemon
25 мая 2018 в 00:18
-1

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

 var funcs = [];
    for (var i = 0; i < 3; i++) {
    // let's 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's run each one to see
    }
nickf
31 мая 2018 в 09:22
2

Потому что это был всего лишь пример проблемы.

avatar
Mark Manning
28 марта 2018 в 20:15
0

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

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

    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» и так далее. Я использую эту технику в подпрограмме для отправки команд при наведении указателя мыши на другую подпрограмму.

avatar
Eyal Segal
14 марта 2018 в 06:19
0

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

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

Но все будет иначе.

avatar
Brooks DuBois
25 февраля 2018 в 03:15
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. Это как-то вовремя обновляет состояние i, чтобы дать вам желаемый результат. Я сделал это случайно, но это имеет смысл, учитывая предыдущие ответы.

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

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

nickf
15 марта 2018 в 21:04
0

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

Brooks DuBois
16 марта 2018 в 00:51
0

правильно, что имеет смысл, учитывая другие ответы об области видимости, но все же является своего рода нелогичным

Ashish Kamble
11 октября 2019 в 09:22
0

Вы перезаписываете значение i с 3 на 0,1,2,3 и вызываете сразу с использованием этих значений @nickf, значит ли это, что j=0 тоже будет funcs[0]

avatar
16 января 2018 в 14:29
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.

enter image description here

enter image description here

Заключение:

  1. Когда вы объявляете переменную с помощью 'var' вне функции, она становится глобальной переменной (вы можете проверить, набрав 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' определено там, и странно то, что для каждой функции значение if i отличается (0, 1, 2).

enter image description here

Заключение :

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

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

avatar
sidhuko
13 января 2018 в 13:17
10

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

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>
avatar
neatsu
13 октября 2017 в 08:53
-2

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

let arr = [1,2,3];

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

arr.forEach(myFunc);

avatar
jsbisht
17 июля 2017 в 10:10
2

СЧЕТЧИК БЫЛ ПЕРВИЧНЫМ

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

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

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

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

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

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

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

СЧЕТЧИК ОБЪЕКТА

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

// ****************************
// 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
avatar
Pawel
28 февраля 2017 в 15:09
3

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

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

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

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

Pawel
27 декабря 2017 в 01:52
0

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

user4639281
27 декабря 2017 в 02:36
2

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

avatar
Vikash Singh
20 января 2017 в 10:02
8

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

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}
avatar
Costa Michailidis
7 декабря 2016 в 17:33
24

Функции 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 в нашем случае, считается объявленной в фигурных скобках.)

Modermo
5 апреля 2017 в 02:50
1

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

avatar
Alexander Levakov
11 ноября 2016 в 09:43
0

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

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]();
}
lovasoa
12 ноября 2016 в 10:32
3

Это медленно, потенциально небезопасно и работает не везде.

avatar
Prithvi Uppalapati
7 ноября 2016 в 11:25
13

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

var funcs = [];
for (let i = 0; i < 3; i++) {          // let's 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's run each one to see
}

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

user4639281
27 декабря 2017 в 03:05
0

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

avatar
Buksy
4 ноября 2016 в 08:58
3

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

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's 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's run each one to see
}

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

Это лишь один из множества способов решить эту проблему.

avatar
Ali Kahoot
4 ноября 2016 в 07:46
8

Прежде всего, разберитесь, что не так с этим кодом:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's 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's run each one to see
}

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

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

  1. Мы можем инициализировать 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]();                        
    }
    
  2. Другой вариант - ввести 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]();                        
    }
    
avatar
pixel 67
5 мая 2016 в 11:48
4

И еще одно решение: вместо создания другого цикла просто привяжите 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
}

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

avatar
Rax Wunter
17 декабря 2015 в 14:14
4

Я предпочитаю использовать функцию 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
}

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

Quentin
17 декабря 2015 в 14:24
0

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

Rax Wunter
17 декабря 2015 в 14:31
0

Это связано именно с упомянутой проблемой: как безопасно выполнять итерацию без проблем с закрытием.

Quentin
17 декабря 2015 в 14:31
0

Теперь он не кажется существенно отличным от принятого ответа.

Rax Wunter
17 декабря 2015 в 14:34
0

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

Rax Wunter
17 декабря 2015 в 14:45
0

@Quentin Я бы порекомендовал изучить решение, прежде чем использовать

avatar
Rune FS
17 июня 2015 в 12:02
4

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

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

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

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

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

Rune FS
18 июня 2015 в 18:21
1

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

jherax
27 октября 2015 в 04:07
1

Что такое Query.range(0,3)? Это не часть тегов для этого вопроса. Кроме того, если вы используете стороннюю библиотеку, вы можете предоставить ссылку на документацию.

Rune FS
27 октября 2015 в 10:17
1

@jherax это или, конечно, очевидные улучшения. Спасибо за комментарий. Могу поклясться, что ссылка уже была. Думаю, без этого поста было бессмысленно :). Моя первоначальная идея сохранить это было потому, что я не пытался продвигать использование моей собственной библиотеки, а скорее декларативную идею. Однако задним числом я полностью согласен с тем, что ссылка должна быть там

avatar
axelduch
6 мая 2015 в 22:33
2

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

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

avatar
woojoo666
10 апреля 2015 в 09:57
174

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

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

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

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

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

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

Повторюсь для выделения:

JavaScript не имеет области блока. Переменные, введенные с блоком, ограничены функцией или сценарием

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

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

Именно поэтому эта ошибка так сложна. Даже если вы повторно объявляете переменную, Javascript не выдаст ошибку, а JSLint даже не выдаст предупреждение. Вот почему лучший способ решить эту проблему - воспользоваться преимуществами замыканий, что, по сути, заключается в том, что в Javascript внутренние функции имеют доступ к внешним переменным, потому что внутренние области видимости «охватывают» внешние области.

Closures

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

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

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

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

Обновить

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

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

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

user4639281
27 декабря 2017 в 03:12
6

Теперь в JavaScript есть такая вещь, как область видимости блока с использованием ключевых слов let и const. Если бы этот ответ был расширен, чтобы включить это, на мой взгляд, он был бы гораздо более полезен в глобальном масштабе.

woojoo666
1 марта 2018 в 22:44
0

@TinyGiant, конечно, я добавил некоторую информацию о let и связал более полное объяснение

nutty about natty
14 мая 2018 в 19:08
0

@ woojoo666 Может ли ваш ответ также работать для вызова двух чередующихся URL-адресов в цикле, например: i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (можно заменить window.open () на getelementbyid ......)

woojoo666
3 июня 2018 в 22:58
0

@nuttyaboutnatty извините за такой поздний ответ. Не похоже, что код в вашем примере уже работает. Вы не используете i в своих функциях тайм-аута, поэтому вам не нужно закрытие

woojoo666
8 июня 2018 в 11:22
0

упс, извините, я хотел сказать "похоже, что код в вашем примере уже работает"

avatar
Christian Landgren
9 декабря 2014 в 22:24
10

Я удивлен, что никто еще не предложил использовать функцию 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 вместо map.

JLRishe
31 марта 2015 в 19:59
3

.forEach() - гораздо лучший вариант, если вы на самом деле ничего не сопоставляете, и Дэрил посоветовал это за 7 месяцев до публикации, так что нет ничего удивительного.

jherax
27 октября 2015 в 04:14
0

Этот вопрос не о цикле по массиву

Christian Landgren
11 ноября 2015 в 21:25
0

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

avatar
wpding
14 июля 2014 в 14:42
15

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

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

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

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's 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, либо используйте анонимную функцию, как это сделал Бьорн:

funcs = {};
function outer(i) {              // function outer's 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 при внутреннем выполнении.

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

wpding
26 апреля 2017 в 14:19
0

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

avatar
Daryl
3 мая 2014 в 03:42
34

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

var funcs = [];
[0,1,2].forEach(function(i) {          // let's 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's run each one to see
}

Печать:

My value: 0
My value: 1
My value: 2
avatar
Travis J
5 марта 2014 в 23:03
30

Основная проблема с кодом, показанным 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); 
    };
};
avatar
neurosnap
11 октября 2013 в 18:23
284

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

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.

Kyle Falconer
10 января 2014 в 16:45
10

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

Nico
30 ноября 2014 в 13:17
5

Как бы вы использовали эту технику для определения массива funcs , описанного в исходном вопросе?

JLRishe
31 марта 2015 в 20:54
0

@Nico Так же, как показано в исходном вопросе, за исключением того, что вы использовали бы index вместо i.

Nico
1 апреля 2015 в 09:22
0

@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }

JLRishe
1 апреля 2015 в 09:40
0

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

Nico
1 апреля 2015 в 09:45
0

@JLRishe, ваша версия лучше. Я думаю, однако, что мы не должны рекомендовать этот ответ; использование связывания проще и удобочитаемее.

JLRishe
1 апреля 2015 в 09:53
0

@Nico В общем случае да .bind() лучше, но при итерации по массиву .forEach() почти всегда лучший вариант.

JLRishe
1 апреля 2015 в 10:05
1

@Nico В частном случае OP они просто перебирают числа, так что это не лучший случай для .forEach(), но в большинстве случаев, когда вы начинаете с массива, forEach() является хороший выбор, например: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });

Carsten Farving
22 сентября 2015 в 13:19
0

@Nico На самом деле этот пример решил мою конкретную проблему, когда мне для одного значения нужно было сделать HTTP-запрос и сохранить ссылку для использования в обратном вызове, и .bind, ни forEach не были чистым решением.

Timar Ivo Batis
13 декабря 2018 в 15:51
0

Я искал синтаксис немедленно вызываемой функции и не мог его понять. +1

avatar
Aust
11 октября 2013 в 16:41
372

Другой способ, который еще не упоминался, - это использование 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]();
}

ОБНОВЛЕНИЕ

Как указано в @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]();
}
Bjorn
8 декабря 2014 в 05:18
0

Это то, чем я занимаюсь сейчас, мне также нравятся _.partial с нижним тире / подчеркиванием.

user1106925
28 июня 2015 в 03:29
18

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

mekdev
28 июня 2015 в 18:32
0

Разве это не запускает JsHint - не создавайте функции внутри цикла. ? Я тоже пошел по этому пути, но после запуска инструментов контроля качества кода ничего не вышло.

Aust
29 июня 2015 в 16:23
5

@squint @mekdev - Вы оба правы. Мой первоначальный пример был написан быстро, чтобы продемонстрировать, как используется bind. Я добавил еще один пример по вашим предложениям.

user2290820
11 сентября 2015 в 12:14
5

Я думаю, вместо того, чтобы тратить зря вычисления на два цикла O (n), просто выполните for (var i = 0; i <3; i ++) {log.call (this, i); }

niry
8 января 2017 в 05:55
1

.bind () делает то, что предлагает принятый ответ, PLUS скрипки с this.

Todd Chaffee
10 июня 2019 в 20:48
0

Это злоупотребление bind с целью создания закрытия, а не то, для чего предназначен bind. Просто создайте свое собственное закрытие. Или теперь используйте let в своем цикле for.

avatar
yilmazburk
19 сентября 2013 в 14:20
33

попробуйте этот покороче

  • без массива

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


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

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

http://jsfiddle.net/7P6EN/

Kemal Dağ
28 июня 2015 в 08:51
1

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

avatar
Kemal Dağ
25 июня 2013 в 14:21
52

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

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

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

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

, который 3 раза предупреждает "2". Это связано с тем, что анонимные функции, созданные в цикле 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 отличается для каждой функции, определенной внутри анонимной функции.

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

DanMan
26 июля 2013 в 11:18
2

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

Kemal Dağ
26 июля 2013 в 12:20
1

@DanMan Спасибо. Самовызывающиеся анонимные функции - очень хороший способ решить проблему отсутствия в JavaScript области видимости переменных уровня блока.

jherax
27 октября 2015 в 04:29
3

Самовызов или самовызов - не подходящий термин для этого метода, точнее IIFE (выражение немедленного вызова функции). Ссылка: benalman.com/news/2010/11/…

avatar
Ben McCormick
21 мая 2013 в 03:04
162

Теперь, когда 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. Он также поддерживается в Node, и вы можете использовать его где угодно, воспользовавшись преимуществами таких инструментов сборки, как Babel. Здесь вы можете увидеть рабочий пример: http://jsfiddle.net/ben336/rbU4t/2/

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

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

MattC
23 февраля 2016 в 17:47
0

К сожалению, let еще не полностью поддерживается, особенно на мобильных устройствах. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…

Dan
22 июня 2016 в 10:18
3

По состоянию на июнь 2016 года let поддерживается во всех основных версиях браузеров, кроме iOS Safari, Opera Mini и Safari 9. Браузеры Evergreen поддерживают его. Babel будет правильно транслировать его, чтобы сохранить ожидаемое поведение без включенного режима высокой совместимости.

Ben McCormick
27 июня 2016 в 14:24
0

@DanPantry, да, пора обновить :) Обновлено, чтобы лучше отразить текущее положение вещей, включая добавление упоминания о константах, ссылках на документы и улучшенной информации о совместимости.

pixel 67
19 марта 2018 в 15:56
0

Разве не поэтому мы используем babel для транспиляции нашего кода, чтобы браузеры, не поддерживающие ES6 / 7, могли понять, что происходит?

avatar
user1724763
20 апреля 2013 в 09:59
60

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

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

Charidimos
21 сентября 2019 в 09:53
0

Небольшое уточнение: в первом примере замыканий в циклах счетчики [0] и счетчики [1] не являются независимыми не из-за соображений производительности. Причина в том, что var obj = {counter: 0}; оценивается перед выполнением любого кода, как указано в: MDN var: объявления var, где бы они ни происходили, обрабатываются до выполнения любого кода.

avatar
Boann
7 августа 2012 в 08:45
64

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

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

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

midnite
3 декабря 2013 в 02:56
0

Спасибо, и ваше решение работает. Но я хотел бы спросить, почему это работает, но поменять местами строку var и строку return не получится? Спасибо!

Boann
3 декабря 2013 в 04:35
0

@midnite Если вы поменяли местами var и return, тогда переменная не будет назначена до того, как она вернет внутреннюю функцию.

avatar
Darren Clark
15 апреля 2009 в 06:48
93

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

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

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

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

avatar
eglasius
15 апреля 2009 в 06:25
77

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

Обертывание его в функцию, которая оценивает возвращение функции, например, ответ 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]();
}
eglasius
15 апреля 2009 в 06:54
0

@nickf какой браузер? Как я уже сказал, у него есть проблемы с совместимостью, я имею в виду серьезные проблемы совместимости, например, я не думаю, что let поддерживается в IE.

eglasius
16 апреля 2009 в 02:55
1

@nickf да, проверьте эту ссылку: developer.mozilla.org/En/New_in_JavaScript_1.7 ... проверьте раздел определений let, внутри цикла есть пример onclick

eglasius
16 апреля 2009 в 02:58
2

@nickf хмм, на самом деле вам нужно явно указать версию: <script type = "application / javascript; version = 1.7" /> ... Я фактически нигде не использовал его из-за ограничения IE, это просто не практично :(

eglasius
16 апреля 2009 в 03:06
0

вы можете увидеть поддержку браузером для различных версий здесь es.wikipedia.org/wiki/Javascript

avatar
jottos
15 апреля 2009 в 06:18
9

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

avatar
Bjorn
15 апреля 2009 в 06:10
401

Попробуйте:

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 - лучший способ сделать это сейчас. Также есть _.partial с нижним тире / подчеркиванием, если вам не нужно или вы не хотите возиться с thisArg bind.

aswzen
6 апреля 2018 в 01:32
3

какие-либо объяснения по поводу }(i));?

Jet Blue
26 июля 2018 в 22:01
3

@aswzen Я думаю, что он передает i в качестве аргумента index функции.

Abhishek Singh
15 марта 2019 в 15:17
0

он фактически создает индекс локальной переменной.

Eggs
14 апреля 2020 в 05:51
1

Немедленно вызвать выражение функции, также известное как IIFE. (i) - это аргумент анонимного функционального выражения, которое вызывается немедленно, и индекс становится установленным с i.