Питання Замикання 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.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

Це видає це:

Моє значення: 3
  Моє значення: 3
  Моє значення: 3

Враховуючи, що я хотів би вивести його:

Моє значення: 0
  Моє значення: 1
  Моє значення: 2


Така ж проблема виникає, коли затримка запуску функції викликана використанням слухачів подій:

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

Яке рішення цієї основної проблеми?


2324
2018-04-15 06:06


походження


Ви впевнені, що не хочете funcs бути масивом, якщо ви використовуєте числові показники? Просто голови вгору. - DanMan
Це дійсно заплутана проблема. Це Стаття допоможе мені зрозуміти це. Це може допомогти і іншим. - user3199690
Інше просте та пояснене рішення: 1) Вкладені функціїмати доступ до сфери "вище" їх; 2) a закриття рішення... "Закриття - це функція, що має доступ до батьківської області, навіть після того, як батьківська функція закрита". - Peter Krauss
Посилання на цю посилання для кращого нерозуміння javascript.info/tutorial/advanced-functions - Saurabh Ahuja
В ES6, тривіальним рішенням є оголошення змінної я з дозволяти, що залежить від тіла петлі. - Tomas Nikodym


Відповіді:


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

Оскільки в JavaScript не існує блочного діапазону - лише функціональна область - шляхом обертання створення функції в новій функції, ви гарантуєте, що значення "i" залишатиметься таким, як ви задумали.


2015 Рішення: для кожного

З досить широкою доступністю 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), новітня версія JavaScript, тепер починає застосовуватися у багатьох вічнозелених браузерах та бекендальних системах. Існують також трансплікатори, як Вавилон що перетворить ES6 на ES5, щоб дозволити використання нових функцій у старих системах.

ES6 впроваджує нові let і const ключові слова, які розрізняються за var-засновані змінні. Наприклад, у циклі з a let-оснований індекс, кожна ітерація через цикл матиме нове значення i де кожне значення входить у цикл, тому ваш код буде працювати, як ви очікуєте. Є багато ресурсів, але я рекомендую Блок-обчислювальний пост 2-х місцевості як чудовий джерело інформації.

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 нарешті отримує це правильно.


1799
2018-04-15 06:18



це не так function createfunc(i) { return function() { console.log("My value: " + i); }; } все ще закривається, оскільки він використовує змінну i? - アレックス
На жаль, ця відповідь застаріла, і ніхто не побачить правильної відповіді внизу - використання Function.bind() безумовно, краще зараз, див stackoverflow.com/a/19323214/785541. - Wladimir Palant
@Wladimir: Ваша пропозиція, що .bind() є "правильна відповідь" не правильно. У кожного є своє місце. З .bind() ви не можете зв'язувати аргументи без зобов'язання this вартість Також ви отримаєте копію i аргумент без можливості змінювати його між дзвінками, які іноді потрібні. Отже, вони абсолютно різні конструкції, не кажучи вже про це .bind() реалізація була історично повільною. Звичайно, в простому прикладі або працюватиме, але закриття - це важлива концепція, яку можна зрозуміти, і це питання. - cookie monster
Будь ласка, припиніть використовувати ці хакери для функції повернення, використовуйте [] для будь-кого або [] .map замість того, щоб уникнути повторного використання однакових змінних. - Christian Landgren
@ ChristianLandgren: Це дуже корисно, якщо ви повторюєте масив. Ці методи не є "хаками". Вони є важливими знаннями.


Спробуйте:

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's більш пізня відповідь про використання .bind це найкращий спосіб зробити таку справу зараз. Існує також ло-тире / підкреслення _.partial коли ти не потребуєш або не хочеш зламати bindс thisArg.


340
2018-04-15 06:10



будь-яке пояснення про }(i)); ? - aswzen
@Awzen Я думаю, що він проходить i як аргумент index до функції. - Jet Blue


Ще один спосіб, який ще не згадувався, - це використання 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]();
}


310
2017-10-11 16:41



Це те, що я роблю і в ці дні, мені також подобається ло-тире / підкреслення _.partial - Bjorn Tipling
.bind() буде значною мірою застарілою з функціями ECMAScript 6. Крім того, це фактично створює дві функції на ітерацію. По-перше, анонімний, а потім створений .bind(). Краще використовувати це, щоб створити його поза циклу, потім .bind() це всередині
Чи не цей тригер JsHint - Не створювати функції в циклі. ? Я також пішов на цей шлях, але після того, як запустив інструменти якості коду, його не було. - mekdev
@squint @mekdev - Ви обидва правильні. Мій початковий приклад написано швидко, щоб продемонструвати, як це зробити bind використовується. Я додав ще один приклад за вашими пропозиціями. - Aust
Я думаю, замість того, щоб витрачати обчислення на два O (n) цикли, просто виконайте для (var i = 0; i <3; i ++) {log.call (це, i); } - user2290820


Використання а Безпосередньо викликана функція вираження, найпростіший та найбільш зрозумілий спосіб додавання індексної змінної:

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.


234
2017-10-11 18:23



Для подальшого читання коду і для уникнення плутанини щодо якого i це те, що я б перейменував параметр функції на index. - Kyle Falconer
Як би ви використали цю техніку для визначення масиву Фучс описується в оригінальному питанні? - Nico
@Nico Точно так само, як показано в оригінальному питанні, крім того, що ви б використовували index замість i. - JLRishe
@ 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](); } - Nico
@Nico У конкретному випадку OP вони просто повторюють цифри, тому це не буде чудовим приводом .forEach(), але багато часу, коли хтось починає масив, forEach() це хороший вибір, наприклад: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; }); - JLRishe


Біт пізно на вечірку, але я вивчав цю проблему сьогодні і помітив, що багато хто з відповідей не повністю вказують на те, як 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 змінні не стикаються і не переписують один одного. Кілька невеликих оптимізацій дає остаточну відповідь, що багато інших користувачів СО дали:

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

Подивіться, як це просто зараз! Для отримання додаткової інформації див ця відповідь, на якому моя інформація заснована на.


127
2018-04-10 09:57



Мені подобається, як ви пояснили і IIFE шлях. Я шукав цього. Дякую. - CapturedTree
В даний час є таке поняття, як визначення розміру блоку в JavaScript за допомогою let і const ключові слова. Якщо б ця відповідь була розширена, щоб включити її, на мій погляд вона була б набагато кориснішою. - Tiny Giant
@ 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 ......) - nutty about natty
@nuttyaboutnatty вибачте про таку пізню відповідь. Здається, що код у вашому прикладі вже працює. Ви не використовуєте i у вашій таймаут функції, тому вам не потрібно закривати - woojoo666


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

Документи тут:

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


121
2018-05-21 03:04



На жаль, "let" все ще не підтримується, особливо в мобільних пристроях. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... - MattC
Станом на 16 червня дозволяти підтримується у всіх основних версіях браузера, окрім iOS Safari, Opera Mini і Safari 9. Підтримка цього сайту забезпечується віртуозними браузерами. Вавіл переведе його правильно, щоб зберегти очікувану поведінку без увімкнення режиму високої відповідальності. - Dan Pantry
@ ДанПентрі, да, про час для оновлення :) Оновлено, щоб краще відображати поточний стан справ, включаючи додавання згадки про const, посилання на документи та кращу інформацію про сумісність. - Ben McCormick
Хіба це не тому, що ми використовуємо babel для перекодування нашого коду, щоб браузери, які не підтримують ES6 / 7, можуть зрозуміти, що відбувається? - pixel 67


Інший спосіб сказати це те, що i у вашій функції пов'язана під час виконання функції, а не час створення функції.

Коли ви створюєте закриття, i це посилання на змінну, визначену зовнішньою сферою, а не копію її, як це було, коли ви створили закриття. Вона буде оцінена під час виконання.

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

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


77
2018-04-15 06:48