Закрытие 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 ]

48 голосов
/ 25 июня 2013

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

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

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

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

30 голосов
/ 19 сентября 2013

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

  • без массива

  • без доп. Для цикла


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

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

http://jsfiddle.net/7P6EN/

26 голосов
/ 06 марта 2014

Основная проблема с кодом, показанным 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] 1006 *. Используя ту же логику, должно быть очевидно, что значение 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); 
    };
};
25 голосов
/ 03 мая 2014

Вот простое решение, которое использует 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
22 голосов
/ 07 декабря 2016

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

14 голосов
/ 07 ноября 2016

С новыми функциями 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.

13 голосов
/ 14 июля 2014

После прочтения различных решений я хотел бы добавить, что причина, по которой эти решения работают, заключается в том, чтобы опираться на концепцию scope scope . Это способ, которым 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 did, либо используйте анонимную функцию, такую ​​как Bjorn did:

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 при внутреннем выполнении.

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

9 голосов
/ 13 января 2018

Этот вопрос действительно показывает историю 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>
9 голосов
/ 10 декабря 2014

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

8 голосов
/ 04 ноября 2016

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

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]();                        
    }
    
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...