zasięg var i let w pętli, closures

zasięg var i let w pętli, closures
adams0
  • Rejestracja:prawie 8 lat
  • Ostatnio:7 dni
  • Postów:316
1

Szanowni!

Wszyscy wiemy co zwróci każda z tych dwóch pętli:

Kopiuj
for (var i = 0; i < 4; i++) {
    setTimeout(function() {
    console.log(i)
    }, 3000)
}

for (let i = 0; i < 4; i++) {
    setTimeout(function() {
    console.log(i)
    }, 3000)
}

Z filmów tych szanownych gentleman'ów:
www.youtube.com/watch?v=-xqJo5VRP4A
www.youtube.com/watch?v=YvJY6z6Xwr4

Dowiedziałem się co nieco o domknięciach.
Wiem również jaki jest zasięg działania ** let** i var (*const też ;-) *)

Nie potrafię jednak zrozumieć tego zasięgu w pętli.

Wyobrażam to sobie jakby var było deklarowane przed pętlą i potem za każdym "obrotem" była przypisywana do zmiennej inna wartość.
A let zachowuje się trochę jakby było deklarowane od nowa z każdą iteracją. (tak, wiem że tak się nie da)

Jest to z pewnością wyobrażenie błędne.

Czy byłby mi ktoś w stanie zilustrować prostymi słowami jak działa zasięg tych zmiennych w pętli?

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

W gruncie rzeczy dobrze rozumiesz - var powoduje hoisting zmiennej, więc na przykład:

Kopiuj
for (var i = 0; i < 10; ++i);

console.log(i); // wyświetli `10`, ponieważ deklaracja zmiennej `i` została wyniesiona przed pętlę
Kopiuj
for (let i = 0; i < 10; ++i);

console.log(i); // wyświetli `i is not defined`, ponieważ czas życia zmiennej `i` obejmuje wyłącznie pętlę

Ale już:

Kopiuj
let i;

for (i = 0; i < 10; ++i);

console.log(i); // wyświetli: 10

Wracając do Twoich przykładów - jako że w drugim przypadku (z wykorzystaniem let) i jest żywe tylko wewnątrz danej iteracji pętli, tworzone domknięcie jest wiązane właśnie z daną wartością i.

Gdybyśmy nieco ten przykład przerobili (dokonali takiego ręcznego hoistingu):

Kopiuj
let i;

for (i = 0; i < 4; ++i) {
  setTimeout(function() {
    console.log(i);
  }, 3000);
}

... to już wszystkie cztery funkcje wyświetlą 4, ponieważ zostaną związane ze zmienną i z wyższego scope'u (spoza samej pętli). która będzie miała wtedy taką właśnie wartość.


edytowany 1x, ostatnio: Patryk27
neves
  • Rejestracja:prawie 22 lata
  • Ostatnio:około 11 godzin
  • Lokalizacja:Kraków
  • Postów:1114
0

Twoje wyobrażenie jest jak najbardziej prawidłowe, var tworzy pojedyńczy binding "storage space", a let i const dla każdej iteracji mają nowy binding.


0

tak poza konkursem spytam, dlatego tu uzyta jest function()?

Kopiuj
for (let i = 0; i < 4; i++) {
    setTimeout(function() {
    console.log(i)
    }, 3000)
}

nie można tego zapisać po prostu

Kopiuj
for (let i = 0; i < 4; i++) {
    setTimeout(console.log(i, 3000))
}
0

zrobiła mi sie literówka z nawiasami w drugim ploku kodu

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0
Kopiuj
setTimeout(console.log(...), 1000);
// ^ spowoduje uruchomienie `console.log()` *od razu*

setTimeout(function() { console.log(...) }, 1000);
// ^ spowoduje uruchomienie `console.log()` po sekundzie

edytowany 1x, ostatnio: Patryk27
0

@Patryk27: dlaczego tak się dzieje? Funkcje mają jakąś odroczoną możliwośc wykonania, czy wynika to z czegoś innego?

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

Wyobraź sobie coś takiego (https://ideone.com/WGtbHp):

Kopiuj
funkcjaA(funkcjaB());

Naturalne jest, że funkcjaB() musi się wykonać i zwrócić jakąś wartość, która stanie się argumentem dla funkcji funkcjaA().

Rozważmy jednak następujący przypadek (https://ideone.com/qtbAld):

Kopiuj
funkcjaA(funkcjaB);

Tutaj sytuacja jest zgoła inna - do funkcjaA() nie przekazujemy rezultatu działania funkcjaB(), tylko wskaźnik na tę funkcję.
Innymi słowy: zapis funkcjaA(funkcjaB); nie powoduje uruchomienia funkcji funkcjaB.

Zamień funkcjaA na setTimeout, funkcjaB na console.log i będziesz miał odpowiedź ;-)


edytowany 1x, ostatnio: Patryk27
adams0
  • Rejestracja:prawie 8 lat
  • Ostatnio:7 dni
  • Postów:316
0

Jest jeszcze jeden przypadek którego nie rozumiem:
Funkcja

Kopiuj
function logaj() {console.log(i)}

for (let i = 0; i < 5; i++){
    setTimeout(logaj, 3e3)
}

wyrzuca błąd że " i is not defined"
Dla czego? W końcu ta funkcja znajduję się w zasięgu zmiennej i ?

SP
  • Rejestracja:ponad 9 lat
  • Ostatnio:ponad 2 lata
  • Postów:127
0

Najlepsza, uniwersalna reguła dotycząca var jest taka, żeby tego po prostu nie używać :)
Ps. można zrobić tak:

Kopiuj
setTimeout( () => console.log(i), 3000);
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

W tym wypadku logaj nie jest domknięciem, tylko zwyczajną funkcją, dlatego też nie widzi zmiennej i (ponieważ nie jest ona ani zmienną globalną, ani lokalną dla logaj, ani parametrem funkcji logaj).

Kontekst ma znaczenie tylko w przypadku domknięć (funkcji anonimowych).


edytowany 1x, ostatnio: Patryk27
DE
  • Rejestracja:ponad 9 lat
  • Ostatnio:11 miesięcy
  • Postów:1788
0

Trudno Ci to zrozumieć, bo nie bardzo kminisz jak działa zasięg i domknięcia. Przeczytaj i jak coś będzie nie jasne, to wróć ;)

https://github.com/getify/You-Dont-Know-JS/tree/master/scope%20%26%20closures
https://dmitryfrank.com/articles/js_closures

edytowany 1x, ostatnio: Desu
BU
  • Rejestracja:około 10 lat
  • Ostatnio:23 dni
  • Postów:422
0

Jakiś czas temu miałem podobne wątpliwości. W sposób abstrakcyjny próbowałem napisać kod robiący to samo.

Gdyby zadeklarowana została dodatkowa zmienna zużyciem let wewnątrz bloku funkcji, to wyświetlone zostaną liczby 0, 1, 2. Wydaje mi się, że na podobnej zasadzie działa pętla z deklaracją licznika pętli za pomocą let.

Kopiuj
for (var i = 0; i < 3; i++) {
    let j = i;
    setTimeout(function() {
        console.log(j);
    }, 0);
}

Taki kod można zobrazować w taki sposób:

Kopiuj
{
    let j = 0;
    setTimeout(function() {
        console.log(j);
    }, 0);
}
{
    let j = 1;
    setTimeout(function() {
        console.log(j);
    }, 0);
}
{
    let j = 2;
    setTimeout(function() {
        console.log(j);
    }, 0);
}

Wykonanie funkcji console.log przez funkcję setTimeout powoduje, że kod wyświetlający licznik pętli wykonuje się asynchronicznie, po ukończeniu działania wszystkich synchronicznych instrukcji do wykonania - w tym całej pętli. To sprawia, że licznik pętli, który jest domknięciem, ma już wartość docelową po ukończeniu działania pętli. Jeśli jest użyte var, to zmienna została zadeklarowana raz w zasięgu całej funkcji zawierającej pętlę lub w zasięgu globalnym, a w każdej kolejnej iteracji pętli ponowna deklaracja jest ignorowana, dlatego zmienna ma wartość nadaną jej po ostatniej iteracji. Jeśli jest użyte słowo let, to zmienna została zadeklarowana w bloku pętli, dlatego jej wartości dla kolejnych iteracji są różne po ukończeniu działania pętli.

To samo w prostszy sposób bez użycia setTimeout można pokazać w taki sposób:

Kopiuj
let arr = [];

for (var i = 0; i < 3; i++) {
    arr.push(function () {
        return i;
    });
}

arr.forEach(function (element) {
    var result = element();
    console.log(result);
});

Podejrzewam, że na tej samej zasadzie odbywa się to w pętli zdarzeń, do której setTimeout dodaje funkcję po upływie określonego czasu.

Zimny Młot napisał(a):

tak poza konkursem spytam, dlatego tu uzyta jest function()?

Kopiuj
for (let i = 0; i < 4; i++) {
    setTimeout(function() {
    console.log(i)
    }, 3000)
}

nie można tego zapisać po prostu

Kopiuj
for (let i = 0; i < 4; i++) {
    setTimeout(console.log(i, 3000))
}

setTimeout przyjmuje parametr, który jest funkcją (wywołanie zwrotne), która zostaje wywołana po upływie określonego czasu. console.log(i, 3000) nie jest funkcją, tylko wynikiem tej funkcji, czyli w tym przypadku undefined. Gdybyś chciał wykonać to w taki sposób, jak w Twoim pytaniu, to musiałbyś napisać setTimeout(console.log, 3000), a to nie miałoby sensu, bo nie przekazujesz argumentów do wyświetlenia. Zadziałałoby to, gdybyś napisał setTimeout(console.log.bind(null, i), 3000).

Większość została wytłumaczona już wcześniej, ale tutaj przedstawiłem to z własnymi przykładami i wnioskami.

edytowany 4x, ostatnio: Burmistrz
BU
Funkcja setTimeout też przyjmuje parametry do przekazania wywołaniu zwrotnemu: setTimeout(console.log, 3000, i).
LukeJL
  • Rejestracja:około 11 lat
  • Ostatnio:około 3 godziny
  • Postów:8422
1

Nie potrafię jednak zrozumieć tego zasięgu w pętli.

Ja też tego nie rozumiałem, dopiero przeczytanie specyfikacji EcmaScript trochę mi rozjaśniło. Chociaż dalej nie wiem, czy do końca rozumiem te wszystkie var, let, pętle itp.

Problem tylko, że specyfikacja jest pisana raczej dla twórców silników, a nie dla programistów JavaScript, więc nie jest napisana przystępnym językiem (i raczej odradzam na początek, bo można sobie jeszcze większego zamieszania w głowie narobić, bo w specyfikacji nic nawet się tak samo nie nazywa jak w kodzie).


Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.