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


2819

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>

... или асинхронный код, например, используя Promises:

// 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(i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(f of fns){
  f();
}

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


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

23
Это действительно запутанная проблема. Эта статья поможет мне понять это . Может ли это помочь другим?
user3199690

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

2
Ссылка на эту ссылку для лучшего понимания javascript.info/tutorial/advanced-functions
Saurabh Ahuja

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

Ответы:


2149

Проблема в том, что переменная 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++) {
  // and now let's run each one to see
  funcs[j]();
}

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


Решение ES5.1: forEach

Учитывая относительно широкую доступность 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()функция предоставляет вам аналогичные возможности.


Решение 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). Край 14, наконец, понимает это правильно.


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

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

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

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

32
@ChristianLandgren: Это полезно, только если вы перебираете массив. Эти методы не являются "взломами". Они необходимые знания.

379

Пытаться:

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):

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


2
Любое объяснение о }(i));?
августа

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

это на самом деле создание индекса локальной переменной.
Абхишек Сингх

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

348

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


Это то, что я делаю и в эти дни, мне также нравятся лох-тире / подчеркивание_.partial
Бьорн

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

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

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

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

269

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

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.


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

5
Как бы вы использовали эту технику для определения функций массива, описанных в исходном вопросе?
Нико

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

@JLRishevar 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](); }
Нико

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

164

Немного опоздал на вечеринку, но я изучал этот вопрос сегодня и заметил, что многие ответы не полностью касаются того, как 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 внутренние функции имеют доступ к внешним переменным, поскольку внутренние области «заключают» внешние области.

Затворы

Это также означает, что внутренние функции «держат» внешние переменные и поддерживают их, даже если внешняя функция возвращается. Чтобы использовать это, мы создаем и вызываем функцию-оболочку исключительно для создания новой области, объявляем 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]();
}

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


Мне нравится, как ты объяснил способ IIFE. Я искал это. Спасибо.
CapturedTree

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

@TinyGiant: Конечно, я добавил немного информации letи связал более полное объяснение
woojoo666

@ 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 ......)
Чокнутый о Natty

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

151

Благодаря широкому распространению 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). Край 14, наконец, понимает это правильно.


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

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

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

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

87

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

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

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

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


71

Что вам нужно понять, так это то, что область действия переменных в 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]();
}


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

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

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

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


59

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

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

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


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

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

53

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

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


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

50

Самое простое решение было бы,

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

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

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

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

31

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

  • нет массива

  • нет лишних для цикла


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

30

Вот простое решение, которое использует 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

27

Основная проблема с кодом, показанным 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); 
    };
};

23

Функции 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'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когда выполняется внутри.

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


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

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.


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

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вместо карты.


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

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

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

9

Этот вопрос действительно показывает историю 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

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


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]();                        
    }

8

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

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

7

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

Case1 : использование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]].Expand, что. Вы увидите один названный объект массива "Global", разверните его. Вы найдете 'i'объявленное в объекте свойство, имеющее значение 3.

введите описание изображения здесь

введите описание изображения здесь

Вывод:

  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).

введите описание изображения здесь

Вывод:

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

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


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 и поэтому склоняюсь к его использованию, поэтому не принимайте мои слова в качестве рекомендации для указанной библиотеки только для декларативного подхода :)


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

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

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

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
}

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


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

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

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

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

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

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
}

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


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);
  }
}

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


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

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

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в каждой итерации.

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


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
}

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.