Закрытие JavaScript внутри циклов - простой практический пример - PullRequest
2599 голосов
/ 15 апреля 2009

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

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

Ответы [ 43 ]

1990 голосов
/ 15 апреля 2009

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


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

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

362 голосов
/ 15 апреля 2009

Попробуйте:

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

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

334 голосов
/ 11 октября 2013

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

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

UPDATE

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

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

var funcs = [];

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

for (var j = 0; j < 3; j++) {
  funcs[j]();
}
255 голосов
/ 11 октября 2013

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

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.

151 голосов
/ 10 апреля 2015

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

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

138 голосов
/ 21 мая 2013

С 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, наконец, понимает это правильно.

84 голосов
/ 15 апреля 2009

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

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

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

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

67 голосов
/ 15 апреля 2009

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

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

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

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

51 голосов
/ 20 апреля 2013

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

Функция определяет новую среду

Рассмотрим:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

Для каждого вызова makeCounter, {counter: 0} приводит к созданию нового объекта. Также новая копия obj также создается для ссылки на новый объект. Таким образом, counter1 и counter2 не зависят друг от друга.

Замыкания в петлях

Использовать замыкание в цикле сложно.

Рассмотрим:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

Обратите внимание, что counters[0] и counters[1] не независимы. На самом деле они работают на том же obj!

Это связано с тем, что для всех итераций цикла используется только одна копия obj, возможно, по соображениям производительности. Даже если {counter: 0} создает новый объект в каждой итерации, одна и та же копия obj будет просто обновлена ​​с ссылка на новейший объект.

Решение - использовать другую вспомогательную функцию:

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

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

Подробное обсуждение см. Подводные камни и проблемы с закрытием JavaScript

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...