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

1

Szanowni!

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

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?

1

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

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

console.log(i); // wyświetli `10`, ponieważ deklaracja zmiennej `i` została wyniesiona przed pętlę
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ż:

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

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ść.

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()?

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

nie można tego zapisać po prostu

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

0
setTimeout(console.log(...), 1000);
// ^ spowoduje uruchomienie `console.log()` *od razu*

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

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

0

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

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

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

0

Jest jeszcze jeden przypadek którego nie rozumiem:
Funkcja

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 ?

0

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

setTimeout( () => console.log(i), 3000);
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).

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

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.

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:

{
    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:

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()?

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

nie można tego zapisać po prostu

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.

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

1 użytkowników online, w tym zalogowanych: 0, gości: 1