Закриття JavaScript всередині циклів - простий практичний приклад


2816

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

Правка: Це також видно в 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
Це справді заплутана проблема. Ця стаття допоможе мені зрозуміти це . Можливо, це допоможе і іншим.
користувач3199690

4
Ще одне просте і пояснене рішення: 1) Вкладені функції мають доступ до області "над" ними ; 2) рішення про закриття ... "Закриття - це функція, що має доступ до батьківської області, навіть після того, як батьківська функція закрита".
Пітер Краусс

2
Перейдіть за цим посиланням, щоб краще зрозуміти javascript.info/tutorial/advanced-functions
Saurabh Ahuja

35
У ES6 , тривіальне рішення оголосити змінну I з нехай , яка поширюється до тіла циклу.
Томаш Нікодим

Відповіді:


2146

Ну, проблема в тому, що змінна 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: для кожного

При відносно широкій доступності 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). Edge 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()реалізація була дуже повільною. Звичайно, в простому прикладі це може спрацювати, але закриття є важливою концепцією для розуміння, і саме про це йшлося.
монстр cookie

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коли вам не потрібно або хочете возитися з bind's thisArg.


2
будь-яке пояснення про }(i));?
aswzen

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
Bjorn

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

5
@squint @mekdev - Ви обоє правильні. Мій початковий приклад був написаний швидко, щоб продемонструвати, як bindвін використовується. До ваших пропозицій я додав ще один приклад.
Авст

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

1
.bind () робить те, що прийнята відповідь пропонує підходи ПЛЮС this.
niry

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

//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. Я це шукав. Дякую.
Захоплене

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функції тайм-
аута

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). Edge 14, нарешті, виходить правильно.


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

2
Станом на червень 16 року, нехай він підтримується у всіх основних версіях браузера, крім iOS Safari, Opera Mini та Safari 9. Веб-браузери підтримують його. Babel перекладе це правильно, щоб зберегти очікувану поведінку без включення режиму високої відповідності.
Ден Комора

@DanPantry Так, час про оновлення :) Оновлено, щоб краще відобразити поточний стан речей, включаючи додавання згадок про const, doc-посилання та кращу інформацію про сумісність.
Бен Маккормік

Чи не тому ми використовуємо babel для трансляції нашого коду, щоб браузери, які не підтримують ES6 / 7, зрозуміли, що відбувається?
піксель 67

87

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

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

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

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


71

Що вам потрібно зрозуміти, це сфера застосування змінних у javascript, заснована на функції. Це важлива відмінність, ніж скажімо c #, де у вас є блок блоку, і просто скопіювати змінну до однієї всередині буде працювати.

Обгортання його у функції, що оцінює повернення функції, як відповідь apphacker, зробить трюк, оскільки змінна тепер має область функції.

Також замість var є ключове слово let, яке дозволило б використовувати правило блоку блоку. У цьому випадку визначення змінної всередині for зробить трюк. З цього приводу ключове слово нехай не є практичним рішенням через сумісність.

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 який браузер? як я вже сказав, у нього є проблеми сумісності, маючи на увазі серйозні проблеми сумісності, начебто я не думаю, що дозволена підтримка в IE.
eglasius

1
@nickf так, перевірте це посилання: developer.mozilla.org/En/New_in_JavaScript_1.7 ... перевірте розділ визначень дозволених, є приклад onclick всередині циклу
eglasius

2
@nickf hmm, насправді вам потрібно чітко вказати версію: <script type = "application / javascript; version = 1.7" /> ... Я фактично ніде не використовував її через обмеження IE, він просто не практичні :(
eglasius

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


59

Ось ще одна варіація щодо техніки, схожа на Bjorn's (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тоді змінна не буде призначений , перш ніж він повернувся внутрішню функцію.
Боан

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 рази. Це пояснюється тим, що анонімні функції, створені для циклу, мають однакове закриття, і в цьому закритті значення " 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 ( Impression -Impoked Function Expression) та передати його 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

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

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) {})природно, працюють: Зворотний виклик, переданий природним чином, формує обтікаючу область з правильно пов'язаними в кожній ітерації 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
}

Код у питанні ОП замінено на, letа не var.


constзабезпечує той самий результат, і його слід використовувати, коли значення змінної не зміниться. Однак використання constвнутрішнього ініціалізатора циклу for 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

Причина, по якій ваш оригінальний приклад не працював, полягає в тому, що всі закриття, які ви створили в циклі, посилалися на один кадр. По суті, три методи на одному об'єкті, що мають лише одну 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. Іншим варіантом може бути введення a, 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

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

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

7

Ми перевіримо, що насправді відбувається, коли ви заявляєте varі let по черзі.

Випадок1 : використання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.

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

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

Висновок:

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

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

2
Попередження майбутнім читачам: Ця відповідь неточно застосовує термін Currying . "Проведення кривої - це коли ви розбиваєте функцію, яка бере кілька аргументів на ряд функцій, які беруть частину аргументів." . Цей код нічого подібного не робить. Все, що ви тут зробили, - це взяти код із прийнятої відповіді, перемістити деякі речі, трохи змінити стиль і назвати, а потім назвати його кривим, чого категорично це не так.

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
}

3

Використовуйте 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]();                      
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.