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

zasięg var i let w pętli, closures
adams0
  • Rejestracja:ponad 7 lat
  • Ostatnio:8 dni
  • Postów:308
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:2 dni
  • 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:ponad 7 lat
  • Ostatnio:8 dni
  • Postów:308
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:10 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:dzień
  • 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:26 minut
  • Postów:8410
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).


Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)