Domknięcia i zakres leksykalny a setTimeout()

0

Cześć,

mam pytanie odnośnie domknięć. Czytam sobie YDKJS i nie pojmuję przykładu z setTimeout().

Książka pokazuje taki oto przykład kodu niedziałającego zgodnie z założeniem (chodzi o wyświetlanie aktualnej wartości i, inkrementowanej w pętli, po jednej sekundzie):

for (var i=1; i<=5; i++) {
   setTimeout(function timer() {
   console.log(i);
   }, i*1000);
}

Działająca wersja powyższego wygląda tak:

for (var i = 1; i<=5; i++) {
   (function() {
      var j = i;
      setTimeout(function timer(){
      console.log(j);
      }, j * 1000);
   })();
}

rozumiem, że żeby napisać kod działający zgodnie z założeniami trzeba zrobić nowy zakres domknięcia dla każdej iteracji (bo i to zmienna globalna). Czyli zrobić nową zmienną i przypisać jej wartość i Pytanie: dlaczego tak się dzieje? Czym różni się zmienna j której przypisuje się wartość i od zmiennej i z zakresu globalnego? Innymi słowy dlaczego tak naprawdę potrzebujemy nowego zakresu domknięcia?
Kolejne pytanie: czy w przykładzie nr 2 tak naprawdę dochodzi do utworzenia 5 różnych zmiennych lokalnych? I jak to się dzieje, że przed wywołaniem setTimeout() j ma wartość 5, a potem już zaczyna od 1?

1

Masz tu do czynienia z asynchronicznością:

Kod z funkcji timer jest wywoływany zawsze po wywołaniu reszty kodu (nawet przy timeout == 0). Dlatego w pierwszym przypadku najpierw przeleci cała pętla, zarejestruje callbacki i ustawi odstępy na odpowiednio 1000, 2000, 3000, 4000, 5000 i ostatecznie wartość zmiennej i na 6, dopiero potem wykonają się wszystkie callbacki timer - 5 osobnych funkcji, każda odwołująca się do zmiennej i, teraz już równej 6.

Dzięki dodatkowym domknięciom możesz sprawić, by każda z tych pieciu funkcji zamiast do zmiennej i z zewnętrznego zakresu odwoływała się do lokalnej zmiennej - lub w Twoim przypadku do zmiennej wewnątrz tej okalającej funkcji anonimowej (to też nie jest jedna a pięć osobnych funkcji).

Btw, dodatkowy wraper w postaci funkcji anonimowej nie jest potrzebny, ten sam efekt możesz uzyskać w ten sposób:

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer(j) {
    console.log(j);
  }, i * 1000, i);
}
0

Ok, dzięki, chyba w końcu załapałem. Jeśli dobrze rozumiem to chodzi o to żeby zmienna j zapamiętała wartość i iterowanej w pętli po to żeby potem wskoczyła do pięciu kolejnych wywołań funkcji timer(). Nie mogę się tutaj odwołać do zmiennej i, bo to to jest zmienna z zewnętrznego zakresu i po wszystkich iteracjach pętli (czyli wtedy gdy setTimeout() jest wykonywana) ma po prostu 6.

Ostatnia rzecz - wolę się upewnić - gdy zrobię console.log'a wewnątrz funkcji timer():

for (var i = 1; i<=5; i++) {
   (function() {
      var j = i;
      setTimeout(function timer(){
         console.log(j);
      }, j * 1000);
   })();
}

to rozumiem że to co pokazuje się w konsoli to pięć różnych wywołań tej funkcji wyświetlającej wartość zmiennej j w każdym wywołaniu funkcji timer()?

A co do Twojego przykładu - czy nie lepiej po prostu użyć let? Zakładając, że mogę to zrobić w ES6.

1

rozumiem że to co pokazuje się w konsoli to pięć różnych wywołań tej funkcji wyświetlającej wartość zmiennej j w każdym wywołaniu funkcji timer()

Raczej wywołanie pięciu róznych funkcji, pięć wywołań tej samej funkcji miałbyś jakbyś zamiast funkcji anonimowej przekazał tam zwykłą funkcję utworzoną poza petlą:

function timer(j) {
  console.log(j);
}

for (var i = 1; i <= 5; i++) {
  setTimeout(timer, i * 1000, i);
}

A co do Twojego przykładu - czy nie lepiej po prostu użyć let? Zakładając, że mogę to zrobić w ES6.

Oczywiście, że tak ;)

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

Nie chciałem mieszać. Mało tego - często lepszym rozwiązaniem jest odpalanie kolejnego setTimeout dopiero po zakończeniu poprzedniego, np tak:

function timer(max, i = 1) {
  if (i <= max) {
    setTimeout(() => {
      console.log(i);
      timer(max, ++i);
    }, 1000)
  }
}

timer(5);
0

Do domknięć z YDKJS (jak ktoś dalej nie ogarnia) warto sobie jeszcze doczytać to http://blog.nebula.us/13-javascript-closures-czyli-zrozumiec-i-wykorzystac-domkniecia

2

@Biały Szewc
W linkowanym przez Ciebie artykule autor sam nie bardzo wie czym jest domknięcie.

0

W JavaScript jest coś takiego jak execution context. Jest jeden główny, tzw. global execution context, który jest tworzony przy starcie programu, a każdy kolejny jest tworzony w momencie, kiedy wykonanie programu wejdzie do ciała jakiejś funkcji.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)

W tym wypadku (o ile się nie mylę) the lexical environment to ten czerwony prostokąt.

Selection_006.png

Link do narzędzia ze screena

Rozwiązanie z IIFE (Immediately Invoked Function Expression)

for (var i = 1; i<=5; i++) {
   (function() {
      var j = i;
      setTimeout(function timer(){
         console.log(j);
      }, j * 1000);
   })();
}

działa, ponieważ podczas każdej iteracji tworzony jest nowy execution context, z nową zmienną j. JavaScript trzyma takie domknięcia (czyli to środowisko) tak długo, jak długo jest gdzieś referencja do funkcji, do której to domknięcie należy.

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