Pytanie Zamknięcie JavaScriptu wewnątrz pętli - prosty praktyczny przykład


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
}

Wyprowadza to:

Moja wartość: 3
  Moja wartość: 3
  Moja wartość: 3

Podczas gdy ja chciałbym, aby to wyjście:

Moja wartość: 0
  Moja wartość: 1
  Moja wartość: 2


Ten sam problem występuje, gdy opóźnienie w uruchomieniu funkcji jest spowodowane użyciem detektorów zdarzeń:

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>

... lub kod asynchroniczny, np. używając obietnic:

// 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.
}

Jakie jest rozwiązanie tego podstawowego problemu?


2324
2018-04-15 06:06


pochodzenie


Na pewno nie chcesz funcs być tablicą, jeśli używasz indeksów numerycznych? Tylko głowa. - DanMan
To naprawdę mylący problem. To artykuł pomóż mi to zrozumieć. Może to też pomoże innym. - user3199690
Kolejne proste i wyjaśnione rozwiązanie: 1) Zagnieżdżone funkcje mieć dostęp do zakresu "nad" nimi; 2) za zamknięcie rozwiązanie... "Zamknięcie to funkcja mająca dostęp do zakresu nadrzędnego, nawet po zamknięciu funkcji nadrzędnej". - Peter Krauss
Odwołaj się do tego linku, aby uzyskać lepsze informacje javascript.info/tutorial/advanced-functions - Saurabh Ahuja
W ES6, trywialnym rozwiązaniem jest zadeklarowanie zmiennej ja z pozwolić, która jest dopasowana do treści pętli. - Tomas Nikodym


Odpowiedzi:


Problem polega na tym, że zmienna iw ramach każdej anonimowej funkcji jest przypisana do tej samej zmiennej poza funkcją.

Klasyczne rozwiązanie: zamknięcia

Co chcesz zrobić, to związać zmienną w obrębie każdej funkcji z oddzielną, niezmienną wartością poza funkcją:

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
}

Ponieważ nie ma żadnego zakresu blokowego w JavaScript - tylko zakres funkcji - poprzez zawijanie funkcji tworzenia w nowej funkcji, upewniasz się, że wartość "i" pozostaje taka, jaką zamierzałeś.


Rozwiązanie 2015: forEach

Ze stosunkowo szeroką dostępnością Array.prototype.forEach funkcja (w 2015 r.), warto zauważyć, że w tych sytuacjach związanych z iteracją przede wszystkim nad szeregiem wartości, .forEach() zapewnia czysty, naturalny sposób na uzyskanie wyraźnego zamknięcia dla każdej iteracji. Oznacza to, że przy założeniu, że macie jakąś tablicę zawierającą wartości (odwołania do DOM, obiekty, cokolwiek) i powstaje problem z konfigurowaniem wywołań zwrotnych specyficznych dla każdego elementu, można to zrobić:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

Chodzi o to, że każde wywołanie funkcji wywołania zwrotnego używanej z funkcją .forEach Pętla będzie jego własnym zamknięciem. Parametrem przekazanym do tego programu obsługi jest element tablicy specyficzny dla danego kroku iteracji. Jeśli jest używany w asynchronicznym wywołaniu zwrotnym, nie koliduje z żadnym z innych wywołań zwrotnych ustanowionych na innych etapach iteracji.

Jeśli zdarzy ci się pracować w jQuery, to $.each() funkcja daje podobną możliwość.


Rozwiązanie ES6: let

ECMAScript 6 (ES6), najnowsza wersja JavaScript, zaczyna być wdrażana w wielu wiecznie zielonych przeglądarkach i systemach zaplecza. Istnieją również transpilatory, takie jak Babel który zamieni ES6 na ES5, aby umożliwić korzystanie z nowych funkcji na starszych systemach.

ES6 wprowadza nowe let i const słowa kluczowe o zasięgu innym niż varoparte na zmiennych. Na przykład w pętli z letna podstawie indeksu, każda iteracja za pośrednictwem pętli będzie miała nową wartość i gdzie każda wartość ma zasięg wewnątrz pętli, więc Twój kod będzie działał zgodnie z oczekiwaniami. Jest wiele zasobów, ale polecam Posterunek o zasięgu blokowym 2ality jako świetne źródło informacji.

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

Uważaj jednak, że obsługa IE9-IE11 i Edge przed Edge 14 let ale otrzymuj powyższe błędy (nie tworzą nowego i za każdym razem, więc wszystkie powyższe funkcje logowałyby się 3, gdyby były użyte var). Edge 14 w końcu robi to dobrze.


1799
2018-04-15 06:18



nie jest function createfunc(i) { return function() { console.log("My value: " + i); }; } wciąż zamknięte, ponieważ używa zmiennej i? - アレックス
Niestety, ta odpowiedź jest nieaktualna i nikt nie zobaczy poprawnej odpowiedzi na dole - używając Function.bind() jest zdecydowanie bardziej pożądana, patrz stackoverflow.com/a/19323214/785541. - Wladimir Palant
@Wladimir: Twoja sugestia, że .bind() jest "poprawna odpowiedź" nie jest w porządku. Każdy ma swoje własne miejsce. Z .bind() nie można wiązać argumentów bez wiązania z this wartość. Otrzymasz również kopię i argument bez możliwości mutacji między wywołaniami, które czasami są potrzebne. Są więc zupełnie inne konstrukcje, nie wspominając o tym .bind() wdrożenia były historycznie powolne. Oczywiście na prostym przykładzie albo zadziała, ale zamknięcia są ważnym pojęciem do zrozumienia, i o to właśnie chodziło. - cookie monster
Przestań używać tych funkcji do zwracania funkcji, użyj zamiast tego [] .forEach lub [] .map, ponieważ unikają ponownego użycia tych samych zmiennych zasięgu. - Christian Landgren
@ ChristianLandgren: Jest to użyteczne tylko w przypadku iteracji tablicy. Te techniki nie są "hackami". Są niezbędną wiedzą.


Próbować:

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

Edytować (2014):

Osobiście uważam @ Aust's nowsza odpowiedź na temat używania .bind jest teraz najlepszym sposobem na zrobienie tego rodzaju rzeczy. Są też lo-dash / podkreślenia _.partial kiedy nie potrzebujesz lub chcesz się z tym pogodzić bind„s thisArg.


340
2018-04-15 06:10



wszelkie wyjaśnienia na temat }(i)); ? - aswzen
@aswzen Myślę, że to mija i jako argument index do funkcji. - Jet Blue


Innym sposobem, który nie został jeszcze wspomniany, jest użycie 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]();
}

AKTUALIZACJA

Jak wskazano przez @squint i @mekdev, uzyskujesz lepszą wydajność, najpierw tworząc funkcję poza pętlą, a następnie wiążąc wyniki w pętli.

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



To jest to, co robię również w tych dniach, lubię też lo-dash / podkreślenia _.partial - Bjorn Tipling
.bind() będzie w większości przestarzały z funkcjami ECMAScript 6. Poza tym w rzeczywistości tworzy dwie funkcje na iterację. Najpierw anonimowy, a następnie wygenerowany przez .bind(). Lepszym sposobem byłoby stworzenie go poza pętlą .bind() to wewnątrz.
Czy to nie wyzwala JsHint - Nie twórz funkcji w pętli. ? Poszedłem tą ścieżką również, ale po uruchomieniu narzędzi jakości kodu jej nie ma .. - mekdev
@squint @mekdev - Oboje macie rację. Mój pierwszy przykład został napisany szybko, aby pokazać, jak bind Jest używane. Dodałem kolejny przykład na Twoje sugestie. - Aust
Myślę, że zamiast marnować obliczenia na dwie pętle O (n), po prostu zrób dla (var i = 0; i <3; i ++) {log.call (this, i); } - user2290820


Korzystanie z Natychmiastowe wywołanie funkcji, najprostszy i najbardziej czytelny sposób na zawarcie zmiennej indeksowej:

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

}

To wysyła iterator i do anonimowej funkcji, którą określamy jako index. Tworzy to zamknięcie, gdzie zmienna i zostanie zapisany do późniejszego wykorzystania w dowolnej asynchronicznej funkcji w IIFE.


234
2017-10-11 18:23



Aby uzyskać dalszą czytelność kodu i uniknąć nieporozumień co do których i jest czym, zmieniłbym nazwę parametru funkcji na index. - Kyle Falconer
W jaki sposób użyłbyś tej techniki do zdefiniowania tablicy funcs opisane w pierwotnym pytaniu? - Nico
@ Nico W taki sam sposób, jak pokazano w oryginalnym pytaniu, z wyjątkiem tego, co używałbyś index zamiast 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 W szczególnym przypadku OP, oni po prostu powtarzają liczbę, więc nie byłoby to zbyt dobre .forEach(), ale dużo czasu, gdy zaczynasz od tablicy, forEach() to dobry wybór, na przykład: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; }); - JLRishe


Trochę spóźniłem się na imprezę, ale odkrywałem ten problem już dziś i zauważyłem, że wiele z tych odpowiedzi nie odnosi się do tego, w jaki sposób JavaScript traktuje celowniki, do czego zasadniczo się to sprowadza.

Tak jak wiele innych osób wspomniało, problem polega na tym, że wewnętrzna funkcja odwołuje się do tego samego i zmienna. Dlaczego więc po prostu nie tworzymy nowej zmiennej lokalnej w każdej iteracji i zamiast tego mamy wewnętrzne odwołanie do funkcji?

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

Tak jak poprzednio, gdzie każda funkcja wewnętrzna wyprowadza ostatnią przypisaną wartość i, teraz każda wewnętrzna funkcja po prostu wyprowadza ostatnią przypisaną wartość ilocal. Ale nie każda iteracja powinna mieć własną ilocal?

Okazuje się, że to jest problem. Każda iteracja dzieli ten sam zakres, więc każda iteracja po pierwszej jest po prostu nadpisywana ilocal. Od MDN:

Ważne: JavaScript nie ma zasięgu bloku. Zmienne wprowadzone za pomocą bloku są ograniczone do funkcji lub skryptu zawierającego, a efekty ich ustawiania utrzymują się poza samym blokiem. Innymi słowy, instrukcje blokowe nie wprowadzają zakresu. Chociaż "standalone" bloki są poprawną składnią, nie chcesz używać niezależnych bloków w JavaScript, ponieważ nie robią tego, co myślisz, że robią, jeśli myślisz, że robią coś takiego w Bloku C lub Java.

Powtórzono dla podkreślenia:

JavaScript nie ma zasięgu bloku. Zmienne wprowadzone za pomocą bloku są ograniczone do funkcji lub skryptu zawierającego

Możemy to zobaczyć, sprawdzając ilocal zanim zadeklarujemy to w każdej iteracji:

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

Właśnie dlatego ten błąd jest tak trudny. Mimo że redeclarujesz zmienną, JavaScript nie rzuci błędu, a JSLint nawet nie wyśle ​​ostrzeżenia. Właśnie dlatego najlepszym sposobem rozwiązania tego problemu jest skorzystanie z zamknięć, co jest zasadniczo pomysłem, że w JavaScript funkcje wewnętrzne mają dostęp do zmiennych zewnętrznych, ponieważ zakresy wewnętrzne "obejmują" zakresy zewnętrzne.

Closures

Oznacza to również, że funkcje wewnętrzne "trzymają" zmienne zewnętrzne i utrzymują je przy życiu, nawet jeśli funkcja zewnętrzna powróci. Aby to wykorzystać, tworzymy i wywołujemy funkcję otoki, aby utworzyć nowy zakres, zadeklarować ilocalw nowym zakresie i zwróć wewnętrzną funkcję, która używa ilocal (więcej wyjaśnień poniżej):

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

Utworzenie wewnętrznej funkcji wewnątrz funkcji otoki nadaje wewnętrznym funkcjom prywatne otoczenie, do którego ma dostęp tylko "zamknięcie". Dlatego za każdym razem, gdy wywołujemy funkcję otoki, tworzymy nową wewnętrzną funkcję z własnym oddzielnym otoczeniem, zapewniając, że ilocal zmienne nie kolidują i nie zastępują siebie nawzajem. Kilka drobnych optymalizacji daje ostateczną odpowiedź, którą dał wiele innych użytkowników 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);
    };
}

Aktualizacja

Teraz, gdy ES6 jest już w głównym nurcie, możemy teraz korzystać z nowego let słowo kluczowe do tworzenia zmiennych blokowych:

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

Zobacz, jak łatwo jest teraz! Aby uzyskać więcej informacji, zobacz ta odpowiedź, z której moje informacje są oparte.


127
2018-04-10 09:57



Podoba mi się, jak wyjaśniłeś także sposób IIFE. Szukałem tego. Dziękuję Ci. - CapturedTree
Istnieje teraz coś takiego jak zakres bloków w JavaScript przy użyciu let i const słowa kluczowe. Jeśli ta odpowiedź miałaby się poszerzyć, aby ją uwzględnić, byłaby, moim zdaniem, bardziej użyteczna na całym świecie. - Tiny Giant
@TinyGoście pewni, dodałem trochę informacji let i łączył bardziej kompletne wyjaśnienie - woojoo666
@ woojoo666 Czy twoja odpowiedź może zadziałać również w celu wywołania dwóch naprzemiennych adresów URL w pętli: 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++ }? (może zastąpić window.open () z getelementbyid ......) - nutty about natty
@nuttyaboutnatty przepraszam za tak późną odpowiedź. Wygląda na to, że kod w twoim przykładzie już działa. Nie korzystasz i w funkcjach timeout, więc nie potrzebujesz zamknięcia - woojoo666


Dzięki rozszerzonemu wsparciu ES6 najlepsza odpowiedź na to pytanie uległa zmianie. ES6 zapewnia let i const słowa kluczowe dla tej dokładnej okoliczności. Zamiast bawić się z zamknięciami, możemy po prostu użyć let aby ustawić zmienną zasięgu pętli, taką jak ta:

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

val następnie wskaże obiekt, który jest specyficzny dla tego konkretnego zwrotu pętli, i zwróci poprawną wartość bez dodatkowego zapisu zamknięcia. To oczywiście znacznie upraszcza ten problem.

const jest podobne do let z dodatkowym ograniczeniem, że nazwa zmiennej nie może być ponownie powiązana z nowym odniesieniem po początkowym przypisaniu.

Obsługa przeglądarki jest teraz dostępna dla tych, którzy kierują reklamy na najnowsze wersje przeglądarek. const/let są obecnie obsługiwane w najnowszych przeglądarkach Firefox, Safari, Edge i Chrome. Jest również obsługiwany w węźle Node i można go używać w dowolnym miejscu, korzystając z narzędzi do budowy, takich jak Babel. Możesz zobaczyć działający przykład tutaj: http://jsfiddle.net/ben336/rbU4t/2/

Dokumenty tutaj:

Uważaj jednak, że obsługa IE9-IE11 i Edge przed Edge 14 let ale otrzymuj powyższe błędy (nie tworzą nowego i za każdym razem, więc wszystkie powyższe funkcje logowałyby się 3, gdyby były użyte var). Edge 14 w końcu robi to dobrze.


121
2018-05-21 03:04



Niestety "let" nadal nie jest w pełni obsługiwane, zwłaszcza w telefonii komórkowej. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... - MattC
Od czerwca 16, pozwolić jest obsługiwany we wszystkich głównych wersjach przeglądarek z wyjątkiem Safari na iOS, Opera Mini i Safari 9. Obsługują go przeglądarki Evergreen. Babel dokona prawidłowej transpozycji, aby zachować oczekiwane zachowanie bez włączonego trybu wysokiej zgodności. - Dan Pantry
@DanPantry tak o czasie na aktualizację :) Zaktualizowano, aby lepiej odzwierciedlać bieżący stan rzeczy, w tym dodanie wzmianki o const, linki do dokumentów i lepszą informację o zgodności. - Ben McCormick
Czy nie dlatego używamy Babel do transpozycji naszego kodu, aby przeglądarki, które nie obsługują ES6 / 7, mogły zrozumieć, co się dzieje? - pixel 67


Innym sposobem powiedzenia tego jest, że i w twojej funkcji jest związana w momencie wykonywania funkcji, a nie czasu tworzenia funkcji.

Kiedy tworzysz zamknięcie, i jest odniesieniem do zmiennej zdefiniowanej w zewnętrznym zakresie, a nie jej kopią, tak jak było podczas tworzenia zamknięcia. Zostanie on oceniony w momencie wykonania.

Większość innych odpowiedzi zapewnia sposoby obejścia, tworząc inną zmienną, która nie zmieni dla ciebie wartości.

Pomyślałem, że dodam wyjaśnienie dla jasności. Dla rozwiązania osobiście pójdę z Harto, ponieważ jest to najbardziej oczywisty sposób zrobienia tego z odpowiedzi tutaj. Każdy z zaksięgowanych kodów będzie działał, ale wolałbym, aby fabryka zamknięć nie musiała pisać stosu komentarzy, aby wyjaśnić, dlaczego deklaruję nową zmienną (Freddy i 1800) lub dziwną wbudowaną składnię zamknięcia (apphacker).


77
2018-04-15 06:48





To, co musisz zrozumieć, to zakres zmiennych w javascript opiera się na funkcji. Jest to ważna różnica niż powiedzmy c #, gdzie masz zasięg bloku, i po prostu kopiowanie zmiennej do jednego wewnątrz fora zadziała.

Zawinięcie go w funkcję, która ocenia zwracanie funkcji, takiej jak odpowiedź apphakera, zdziała sztuczkę, ponieważ zmienna ma teraz zakres funkcji.

Istnieje również słowo kluczowe let zamiast var, które pozwoliłoby na użycie reguły zasięgu bloku. W takim przypadku zdefiniowanie zmiennej wewnątrz fora wystarczyłoby. Powiedział, że słowo kluczowe "let" nie jest praktycznym rozwiązaniem ze względu na kompatybilność.

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

61
2018-04-15 06:25



@nickf której przeglądarki? jak już powiedziałem, ma problemy ze zgodnością, mam tu na myśli poważne problemy ze zgodnością, na przykład nie wydaje mi się, aby w IE obsługiwane było let. - eglasius
@ nickf Tak, sprawdź to odniesienie: developer.mozilla.org/En/New_in_JavaScript_1.7 ... sprawdź sekcję let letses, w pętli znajduje się przykład onclick - eglasius
@ nickf hmm, tak naprawdę musisz wyraźnie określić wersję: <script type = "application / javascript; version = 1.7" /> ... W rzeczywistości nie używałem go nigdzie z powodu ograniczeń IE, to po prostu nie jest praktyczny :( - eglasius
Zobacz też Jakie przeglądarki obecnie obsługują słowo kluczowe "let" javascript? - rds
Nie używaj więc wtedy - regisbsb