Trochę za późno na imprezę, ale dzisiaj zgłębiałem ten problem i zauważyłem, że wiele odpowiedzi nie do końca odnosi się do tego, jak JavaScript traktuje zakresy, co w gruncie rzeczy sprowadza się do tego.
Tak jak wielu innych wspomniano, problemem jest to, że funkcja wewnętrzna odnosi się do tej samej i
zmiennej. Dlaczego więc po prostu nie tworzymy nowej zmiennej lokalnej przy każdej iteracji i zamiast tego mamy odwołanie do funkcji wewnętrznej?
//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 generowała ostatnią przypisaną wartość i
, teraz każda funkcja wewnętrzna tylko wyświetla ostatnią przypisaną wartość ilocal
. Ale nie każda iteracja powinna mieć swoją własnąilocal
?
Okazuje się, że to jest problem. Każda iteracja ma ten sam zakres, więc każda iteracja po pierwszej jest po prostu nadpisywana ilocal
. Z MDN :
Ważne: JavaScript nie ma zasięgu blokowego. Zmienne wprowadzone wraz z blokiem mają zakres obejmujący funkcję lub skrypt, a efekty ich ustawienia utrzymują się poza samym blokiem. Innymi słowy, instrukcje blokowe nie wprowadzają zakresu. Chociaż „samodzielne” bloki są poprawną składnią, nie chcesz używać samodzielnych bloków w JavaScript, ponieważ nie robią tego, co myślisz, że robią, jeśli uważasz, że robią coś takiego jak takie bloki w C lub Java.
Podkreślono dla podkreślenia:
JavaScript nie ma zasięgu blokowego. Zmienne wprowadzane za pomocą bloku mają zakres obejmujący funkcję lub skrypt
Możemy to sprawdzić, 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. Nawet jeśli ponownie zmieniasz zmienną, JavaScript nie wyśle błędu, a JSLint nawet nie wyśle ostrzeżenia. Dlatego też najlepszym sposobem rozwiązania tego jest skorzystanie z zamknięć, co jest w istocie ideą, że w Javascripcie funkcje wewnętrzne mają dostęp do zmiennych zewnętrznych, ponieważ wewnętrzne zakresy „otaczają” zewnętrzne zakresy.
Oznacza to również, że funkcje wewnętrzne „trzymają” zewnętrzne zmienne i utrzymują je przy życiu, nawet jeśli funkcja zewnętrzna powróci. Aby to wykorzystać, tworzymy i wywołujemy funkcję otoki wyłącznie w celu utworzenia nowego zakresu, zadeklarowania ilocal
w nowym zakresie i zwrócenia używanej funkcji wewnętrznej 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 funkcji wewnętrznej w funkcji opakowania daje funkcji wewnętrznej prywatne środowisko, do którego tylko on może uzyskać dostęp, „zamknięcie”. Dlatego za każdym razem, gdy wywołujemy funkcję otoki, tworzymy nową funkcję wewnętrzną z własnym oddzielnym środowiskiem, zapewniając, że ilocal
zmienne się nie kolidują i nie nadpisują. Kilka drobnych optymalizacji daje ostateczną odpowiedź, którą podało wielu 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
Dzięki ES6 mainstream możemy teraz używać nowego let
słowa kluczowego do tworzenia zmiennych o zasięgu blokowym:
//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, jakie to jest teraz łatwe! Aby uzyskać więcej informacji, zobacz tę odpowiedź , na której oparte są moje informacje.
funcs
być tablicą, jeśli używasz indeksów numerycznych? Tylko jedna głowa do góry.