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

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

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

7 голосов
/ 16 января 2018

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

Теперь откройте окно консоли хрома , нажав 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», и странно то, что для каждой функции значение, если i, отличается (0, 1, 2).

enter image description here

Вывод:

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

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

6 голосов
/ 20 января 2017

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

var funcs = [];
for (var i = 0; i < 3; i++) {     
  (funcs[i] = function() {         
    console.log("My value: " + i); 
  })(i);
}
3 голосов
/ 17 декабря 2015

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

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

3 голосов
/ 13 июля 2018
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
}
3 голосов
/ 15 апреля 2019

Используйте let (заблокированную область видимости) вместо 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]();                      
}
3 голосов
/ 04 ноября 2016

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

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

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

2 голосов
/ 28 февраля 2017

Многие решения кажутся правильными, но они не упоминают, что он называется 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);
  }
}

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

2 голосов
/ 07 мая 2015

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

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

2 голосов
/ 17 июня 2015

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

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