Zamknięcia JavaScript a funkcje anonimowe


562

Mój przyjaciel i ja obecnie dyskutujemy, co jest zamknięciem w JS, a co nie. Chcemy tylko upewnić się, że naprawdę to rozumiemy.

Weźmy ten przykład. Mamy pętlę zliczania i chcemy wydrukować zmienną licznika na konsoli z opóźnieniem. Dlatego używamy setTimeouti zamykania do przechwytywania wartości zmiennej licznika, aby upewnić się, że nie zostanie wydrukowana N razy wartość N.

Niewłaściwe rozwiązanie bez zamknięć lub czegokolwiek w pobliżu zamknięć to:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

który oczywiście wydrukuje 10-krotność wartości ipo pętli, a mianowicie 10.

Więc jego próba była:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

drukowanie 0 do 9 zgodnie z oczekiwaniami.

Powiedziałem mu, że nie używa zamknięcia do schwytania i, ale upiera się, że tak. Udowodniłem, że nie używa zamknięć , umieszczając ciało pętli for w innym setTimeout(przekazując swoją anonimową funkcję setTimeout), drukując ponownie 10 razy 10. To samo dotyczy, jeśli przechowuję jego funkcję w a vari wykonuję ją po pętli, drukując również 10 razy 10. Więc moim argumentem jest to, że tak naprawdę nie przechwytuje wartościi , czyniąc swoją wersję nie zakończeniem.

Moja próba była:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

Więc wychwytywania I i(o nazwie i2wewnątrz zamknięcia), ale teraz wrócić inną funkcję i przekazać te okolice. W moim przypadku funkcja przekazana do setTimeout naprawdę przechwytuje i.

Kto teraz używa zamknięć, a kto nie?

Zauważ, że oba rozwiązania drukują opóźnienie od 0 do 9 na konsoli, więc rozwiązują pierwotny problem, ale chcemy zrozumieć, które z tych dwóch rozwiązań używa zamknięć, aby to osiągnąć.



1
@leemes: Zobacz moją edycję ninja dla drugiego linku.
Blender

2
właśnie zawarliśmy umowę: ten, kto ma rację, zdobędzie punkty SO związane z tym pytaniem
brillout,

1
@leemes - Oboje używacie zamknięć. Obaj wykonaliście dwie funkcje - funkcję zewnętrzną i funkcję wewnętrzną; a obie twoje wewnętrzne funkcje są zamknięciami. Wszystkie twoje funkcje są lambdas ( funkcje anonimowe ). Przeczytaj moją odpowiedź, aby poznać szczegóły.
Aadit M Shah

1
@ rozwiązywanie problemów - nie mam pojęcia, co to jest zmodyfikowane zamknięcie. Widzę, że twój link wskazuje na kod C #. Czy zmodyfikowane zamknięcia są obsługiwane przez JavaScript?
Aadit M Shah,

Odpowiedzi:


650

Uwaga edytora: Wszystkie funkcje w JavaScript są zamknięciami, jak wyjaśniono w tym poście . Interesuje nas jednak tylko identyfikacja podzbioru tych funkcji, które są interesujące z teoretycznego punktu widzenia. Odtąd wszelkie odniesienia do słowa „ zamknięcie” będą odnosić się do tego podzbioru funkcji, chyba że zaznaczono inaczej.

Proste wyjaśnienie dla zamknięć:

  1. Weź funkcję. Nazwijmy to F.
  2. Wymień wszystkie zmienne F.
  3. Zmienne mogą być dwojakiego rodzaju:
    1. Zmienne lokalne (zmienne powiązane)
    2. Zmienne nielokalne (zmienne bezpłatne)
  4. Jeśli F nie ma wolnych zmiennych, to nie może być zamknięciem.
  5. Jeżeli F ma wolne zmienne (które są określone w w zakresie macierzystego F), a następnie:
    1. Nie może być tylko jeden zakres dominującą F do której wolna zmienna jest związana.
    2. Jeśli odwołanie do F jest spoza tego zakresu nadrzędnego, staje się zamknięciem dla tej zmiennej swobodnej.
    3. Ta wolna zmienna nazywana jest podwyższeniem wartości zamknięcia F.

Teraz użyjmy tego, aby dowiedzieć się, kto używa zamknięć, a kto nie (dla wyjaśnienia nazwałem funkcje):

Przypadek 1: Program twojego przyjaciela

for (var i = 0; i < 10; i++) {
    (function f() {
        var i2 = i;
        setTimeout(function g() {
            console.log(i2);
        }, 1000);
    })();
}

W powyższym programie są dwie funkcje: fi g. Zobaczmy, czy są zamknięciami:

Dla f:

  1. Wymień zmienne:
    1. i2jest zmienną lokalną .
    2. ijest wolną zmienną.
    3. setTimeoutjest wolną zmienną.
    4. gjest zmienną lokalną .
    5. consolejest wolną zmienną.
  2. Znajdź zakres nadrzędny, z którym związana jest każda wolna zmienna:
    1. ijest związany z zasięgiem globalnym.
    2. setTimeoutjest związany z zasięgiem globalnym.
    3. consolejest związany z zasięgiem globalnym.
  3. W jakim zakresie odwołuje się funkcja ? Zakres globalny .
    1. Dlatego inie jest zamknięty przez f.
    2. Dlatego setTimeoutnie jest zamknięty przez f.
    3. Dlatego consolenie jest zamknięty przez f.

Zatem funkcja fnie jest zamknięciem.

Dla g:

  1. Wymień zmienne:
    1. consolejest wolną zmienną.
    2. i2jest wolną zmienną.
  2. Znajdź zakres nadrzędny, z którym związana jest każda wolna zmienna:
    1. consolejest związany z zasięgiem globalnym.
    2. i2jest związany z zakresem f.
  3. W jakim zakresie odwołuje się funkcja ? ZakressetTimeout .
    1. Dlatego consolenie jest zamknięty przez g.
    2. Dlatego i2jest zamknięty przez g.

Zatem funkcja gjest zamknięcie dla zmiennej wolnej i2(który jest dla upvalue g) kiedy to odwołanie od wewnątrzsetTimeout .

Źle dla ciebie: Twój przyjaciel używa zamknięcia. Wewnętrzną funkcją jest zamknięcie.

Przypadek 2: Twój program

for (var i = 0; i < 10; i++) {
    setTimeout((function f(i2) {
        return function g() {
            console.log(i2);
        };
    })(i), 1000);
}

W powyższym programie są dwie funkcje: fig. Zobaczmy, czy są zamknięciami:

Dla f:

  1. Wymień zmienne:
    1. i2jest lokalnyzmienną .
    2. gjest lokalnyzmienną .
    3. consolejest wolną zmienną.
  2. Znajdź zakres nadrzędny, z którym związana jest każda wolna zmienna:
    1. consolejest związany z zasięgiem globalnym.
  3. W jakim zakresie odwołuje się funkcja ? Theglobalny .
    1. Dlatego consolenie jest zamknięty przez f.

Zatem funkcja fnie jest zamknięciem.

Dla g:

  1. Wymień zmienne:
    1. consolejest wolną zmienną.
    2. i2jest wolną zmienną.
  2. Znajdź zakres nadrzędny, z którym związana jest każda wolna zmienna:
    1. consolejest związany z zasięgiem globalnym.
    2. i2jest związany z zakresem f.
  3. W jakim zakresie odwołuje się funkcja ? ZakressetTimeout .
    1. Dlatego consolenie jest zamknięty przezg.
    2. Dlatego i2jest zamknięty przez g.

Zatem funkcja gjest zamknięcie dla zmiennej wolnej i2(który jest dla upvalue g) kiedy to odwołanie od wewnątrzsetTimeout .

Dobrze dla ciebie: używasz zamknięcia. Wewnętrzną funkcją jest zamknięcie.

Więc ty i twój przyjaciel używacie zamknięć. Nie kłóćcie się. Mam nadzieję, że wyjaśniłem pojęcie zamknięć i jak je zidentyfikować dla was obojga.

Edytować: Proste wyjaśnienie, dlaczego wszystkie funkcje są zamknięte (napisy @Peter):

Najpierw rozważmy następujący program (to kontrola ):

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. Wiemy o tym lexicalScopei regularFunctionnie są zamknięciami z powyższej definicji .
  2. Kiedy wykonujemy program , oczekujemy message ostrzeżenia, ponieważ regularFunction nie jest to zamknięcie (tzn. Ma on dostęp do wszystkich zmiennych w zakresie nadrzędnym - w tymmessage ).
  3. Kiedy wykonujemy program , obserwujemy tomessage jest to rzeczywiście zaalarmowane.

Następnie rozważmy następujący program (jest to alternatywa ):

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. Wiemy, że tylko closureFunctionzamknięcie powyższej definicji .
  2. Kiedy wykonujemy program, nie oczekujemy message ostrzeżenia, ponieważ closureFunction jest to zamknięcie (tzn. Ma on dostęp do wszystkich zmiennych nielokalnych w momencie tworzenia funkcji ( zobacz tę odpowiedź) ) - nie obejmuje tomessage ).
  3. Kiedy wykonujemy program , obserwujemy tomessage jest on w rzeczywistości ostrzegany.

Co z tego wywnioskujemy?

  1. Tłumacze JavaScript nie traktują zamknięć inaczej niż sposób, w jaki traktują inne funkcje.
  2. Każda funkcja niesie ze sobą łańcuch zasięgu . Zamknięcia nie mają osobnego środowiska odniesienia.
  3. Zamknięcie jest jak każda inna funkcja. Po prostu zadzwoń im closures gdy są odniesione w zakresie poza zakresem, do którego należą , ponieważ jest to ciekawy przypadek.

40
Zaakceptowano, ponieważ podchodzisz bardzo szczegółowo, wyjaśniając bardzo fajnie, co się dzieje. I wreszcie lepiej zrozumiałem, czym jest zamknięcie, lub lepiej: jak działa zmienne wiązanie w JS.
leemes

3
W przypadku 1 mówisz, że gdziała w zakresie setTimeout, ale w przypadku 2 mówisz, że fdziała w zakresie globalnym. Oba są w ramach setTimeout, więc jaka jest różnica?
rosscj2533

9
Czy podasz w tym celu swoje źródła? Nigdy nie widziałem definicji, w której funkcja może być zamknięciem, jeśli zostanie wywołana w jednym zakresie, ale nie w innym. Zatem definicja ta wydaje się być podzbiorem bardziej ogólnej definicji, do której jestem przyzwyczajony (patrz odpowiedź kev ), gdzie zamknięcie jest zamknięciem jest zamknięciem niezależnie od zakresu, w jakim się nazywa, a nawet jeśli nigdy nie jest wywoływane!
Briguy37

11
@AaditMShah Zgadzam się z tobą, co to jest zamknięcie, ale mówisz tak, jakby była różnica między zwykłymi funkcjami a zamknięciami w JavaScript. Nie ma różnicy; wewnętrznie każda funkcja będzie zawierać odniesienie do określonego łańcucha zasięgu, w którym została utworzona. Silnik JS nie uważa tego za inny przypadek. Nie ma potrzeby skomplikowanej listy kontrolnej; po prostu wiedz, że każdy obiekt funkcji ma zakres leksykalny. Fakt, że zmienne / właściwości są globalnie dostępne, nie czyni zamknięcia mniejszą (jest to po prostu niepotrzebny przypadek).
Peter

13
@Peter - Wiesz co, masz rację. Nie ma różnicy między zwykłą funkcją a zamknięciem. Przeprowadziłem test, aby to udowodnić, a to na twoją korzyść: oto kontrola i oto alternatywa . To, co mówisz, ma sens. Tłumacz JavaScript musi wykonywać specjalne czynności księgowe dla zamknięć. Są to po prostu produkty uboczne języka o leksykalnym zasięgu, z pierwszorzędnymi funkcjami. Moja wiedza była ograniczona do tego, co przeczytałem (co było fałszywe). Dziękuję za poprawienie mnie. Zaktualizuję moją odpowiedź, aby odzwierciedlić to samo.
Aadit M Shah,

96

Zgodnie z closuredefinicją:

„Zamknięcie” to wyrażenie (zazwyczaj funkcja), które może mieć dowolne zmienne wraz ze środowiskiem, które wiąże te zmienne (które „zamyka” wyrażenie).

Używasz, closurejeśli zdefiniujesz funkcję, która używa zmiennej, która jest zdefiniowana poza funkcją. (nazywamy zmienną wolną zmienną ).
Wszystkie używają closure(nawet w pierwszym przykładzie).


1
W jaki sposób trzecia wersja wykorzystuje zmienną zdefiniowaną poza funkcją?
Jon

1
@ Zwraca użycie funkcji i2zdefiniowanej na zewnątrz.
kev

1
@kev Używasz zamknięcia, jeśli zdefiniujesz funkcję, która używa zmiennej, która jest zdefiniowana poza funkcją ...... wtedy w „Przypadku 1: Program twojego przyjaciela” w „Aadit M Shah” odpowiedź brzmi „funkcja f” zamknięcie? wykorzystuje i (zmienną zdefiniowaną poza funkcją). czy zakres globalny odwołuje się do determinatora?
internals-in


54

W skrócie Javascript Zamknięcia pozwolić funkcji na dostęp do zmiennej , która jest zadeklarowana w funkcji leksykalno-dominującej .

Zobaczmy bardziej szczegółowe wyjaśnienie. Aby zrozumieć zamknięcia, ważne jest zrozumienie, w jaki sposób JavaScript mierzy zmienne.

Zakresy

W JavaScript zakresy są definiowane za pomocą funkcji. Każda funkcja określa nowy zakres.

Rozważ następujący przykład;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

wywoływanie f wydruków

hello
hello
2
Am I Accessible?

Rozważmy teraz przypadek, w którym mamy funkcję gzdefiniowaną w innej funkcji f.

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

Zadzwonimy fdo leksykalnego rodzica z g. Jak wyjaśniono wcześniej, mamy teraz 2 zakresy; zakres fi zakres g.

Ale jeden zakres jest „w obrębie” drugiego, więc czy zakres funkcji potomnej jest częścią zakresu funkcji rodzica? Co dzieje się ze zmiennymi zadeklarowanymi w zakresie funkcji nadrzędnej; czy będę mógł uzyskać do nich dostęp z zakresu funkcji potomnej? Właśnie tam wkraczają zamknięcia.

Domknięcia

W JavaScript funkcja gmoże nie tylko uzyskać dostęp do dowolnych zmiennych zadeklarowanych w zakresie, gale także uzyskać dostęp do dowolnych zmiennych zadeklarowanych w zakresie funkcji nadrzędnej f.

Rozważ następujące;

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

wywoływanie f wydruków

hello
undefined

Spójrzmy na linię console.log(foo);. W tym momencie jesteśmy w zakresie gi próbujemy uzyskać dostęp do zmiennej foozadeklarowanej w zakresie f. Ale jak wspomniano wcześniej, możemy uzyskać dostęp do dowolnej zmiennej zadeklarowanej w leksykalnej funkcji nadrzędnej, co ma miejsce tutaj; gjest leksykalnym rodzicem f. Dlatego hellojest drukowany.
Spójrzmy teraz na linię console.log(bar);. W tym momencie jesteśmy w zakresie fi próbujemy uzyskać dostęp do zmiennej barzadeklarowanej w zakresie g. barnie jest zadeklarowany w bieżącym zakresie, a funkcja gnie jest elementem nadrzędnym f, dlatego barjest niezdefiniowana

W rzeczywistości możemy również uzyskać dostęp do zmiennych zadeklarowanych w zakresie leksykalnej funkcji „dziadka”. Dlatego jeśli w funkcji będzie hzdefiniowana funkcjag

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

wtedy hbędzie w stanie uzyskać dostęp do wszystkich zmiennych zadeklarowanych w zakresie funkcji h, gorazf . Odbywa się to przy zamknięciach . W JavaScript zamknięcia pozwalają nam na dostęp do dowolnej zmiennej zadeklarowanej w leksykalnej funkcji nadrzędnej, w leksykalnej funkcji grand-grand, w leksykalnej funkcji grand-grand, itp. Można to postrzegać jako łańcuch zasięgu ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... do ostatniej funkcji nadrzędnej, która nie ma leksykalnego rodzica.

Obiekt okna

W rzeczywistości łańcuch nie kończy się na ostatniej funkcji nadrzędnej. Jest jeszcze jeden specjalny zakres; zakres globalny . Każda zmienna niezadeklarowana w funkcji jest uważana za zadeklarowaną w zasięgu globalnym. Globalny zasięg ma dwie specjalności;

  • każda zmienna zadeklarowana w zasięgu globalnym jest dostępna wszędzie
  • zmienne zadeklarowane w zakresie globalnym odpowiadają właściwościom windowobiektu.

Dlatego istnieją dokładnie dwa sposoby deklarowania zmiennej foo w zasięgu globalnym; albo przez nie deklarowanie go w funkcji, ani przez ustawienie właściwości fooobiektu window.

Obie próby wykorzystują zamknięcia

Po przeczytaniu bardziej szczegółowego wyjaśnienia może się teraz okazać, że oba rozwiązania wykorzystują zamknięcia. Ale dla pewności zróbmy dowód.

Stwórzmy nowy język programowania; JavaScript bez zamykania. Jak sama nazwa wskazuje, JavaScript-No-Closure jest identyczny jak JavaScript, z tym wyjątkiem, że nie obsługuje Zamknięć.

Innymi słowy;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

Dobra, zobaczmy, co się stanie z pierwszym rozwiązaniem z JavaScript-No-Closure;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

dlatego zostanie wydrukowany undefined10 razy w JavaScript-No-Closure.

Dlatego pierwsze rozwiązanie wykorzystuje zamknięcie.

Spójrzmy na drugie rozwiązanie;

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

dlatego zostanie wydrukowany undefined10 razy w JavaScript-No-Closure.

Oba rozwiązania wykorzystują zamknięcia.

Edycja: Zakłada się, że te 3 fragmenty kodu nie są zdefiniowane w zakresie globalnym. W przeciwnym razie zmienne fooi ibyłyby powiązane z windowobiektem, a zatem są dostępne przez windowobiekt zarówno w JavaScript, jak i JavaScript-No-Closure.


Dlaczego nie imożna zdefiniować? Po prostu odwołujesz się do zakresu nadrzędnego, który jest nadal ważny, jeśli nie było żadnych zamknięć.
leemes

z tego samego powodu, dla którego foo jest niezdefiniowane w JavaScript-No-Closure. <code> i </code> nie jest niezdefiniowany w JavaScript dzięki funkcji w JavaScript, która pozwala na dostęp do zmiennych zdefiniowanych w rodzimym języku leksykalnym. Ta funkcja nazywa się zamknięciem.
brillout

Nie zrozumiałeś różnicy między odwoływaniem się do już zdefiniowanych zmiennych i zmiennych wolnych . W zamknięciach definiujemy dowolne zmienne, które muszą być powiązane w kontekście zewnętrznym. W swoim kodzie właśnie ustawiłeś i2 to iw momencie definiowania swojej funkcji. To czyni iNIE zmienną darmową. Nadal uważamy twoją funkcję za zamknięcie, ale bez żadnej zmiennej swobodnej to jest sedno.
leem

2
@leemes, zgadzam się. I w porównaniu z zaakceptowaną odpowiedzią, tak naprawdę nie pokazuje, co się właściwie dzieje. :)
Abel

3
myślę, że jest to najlepsza odpowiedź, wyjaśniająca ogólnie i po prostu zamknięcia, a następnie przeszła do konkretnego przypadku użycia. dzięki!
tim peterson

22

Nigdy nie byłem zadowolony ze sposobu, w jaki ktoś to wyjaśnia.

Kluczem do zrozumienia zamknięć jest zrozumienie, jaki byłby JS bez zamknięć.

Bez zamknięć spowodowałoby to błąd

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

Gdy outerFunc powróci w wyimaginowanej wersji JavaScript z wyłączoną funkcją zamykania, odwołanie do outerVar zostanie wyrzucone na śmietnik i zniknie, pozostawiając nic dla odniesienia do wewnętrznego func.

Zamknięcia są zasadniczo specjalnymi regułami, które uruchamiają się i umożliwiają istnienie tych wariacji, gdy funkcja wewnętrzna odwołuje się do zmiennych funkcji zewnętrznej. W przypadku zamknięć wspomniane zmienne są zachowywane nawet po wykonaniu funkcji zewnętrznej lub „zamknięciu”, jeśli pomaga to zapamiętać punkt.

Nawet przy zamknięciach cykl życia lokalnych zmiennych w funkcji bez wewnętrznych funkcji, które odwołują się do miejscowych, działa tak samo, jak w wersji bez zamknięcia. Po zakończeniu funkcji miejscowi otrzymują śmieci.

Kiedy już masz odniesienie w funkcji wewnętrznej do zewnętrznego var, jest to jednak tak, jakby framuga drzwi przeszkadzała w wyrzucaniu elementów bezużytecznych dla wspomnianych zmiennych.

Być może bardziej dokładnym sposobem spojrzenia na zamknięcia jest to, że funkcja wewnętrzna zasadniczo wykorzystuje zakres wewnętrzny jako własną funkcję zakresu.

Ale kontekst, do którego się odwołuje, jest w rzeczywistości trwały, a nie jak migawka. Wielokrotne uruchamianie zwróconej funkcji wewnętrznej, która stale zwiększa i rejestruje lokalny var funkcji zewnętrznej, będzie ostrzegać o wyższych wartościach.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

Masz rację co do „migawki” (myślę, że odwołujesz się do mojej odpowiedzi). Szukałem słowa, które wskazywałoby na zachowanie. W twoim przykładzie można to postrzegać jako konstrukcję zamykającą „hotlink”. Przechwytując zamknięcie jako parametr funkcji wewnętrznej, można stwierdzić, że zachowuje się jak „migawka”. Ale zgadzam się, że źle użyte słowa tylko wprowadzają zamieszanie do tematu. Jeśli masz jakieś sugestie na ten temat, zaktualizuję moją odpowiedź.
Andries,

Może to pomóc w wyjaśnieniu, jeśli podasz funkcję wewnętrzną jako funkcję nazwaną.
Phillip Senn,

Bez zamknięć wystąpiłby błąd, ponieważ próbujesz użyć zmiennej, która nie istnieje.
Juan Mendes,

Hmm ... dobry punkt. Czy odwołanie się do niezdefiniowanego var nigdy nie spowodowało błędu, ponieważ ostatecznie wyglądałoby jak właściwość obiektu globalnego, czy też mylę się z przypisaniem do niezdefiniowanych zmiennych?
Erik Reppen,

17

Oboje używacie zamknięć.

Idę z definicją Wikipedii tutaj:

W informatyce zamknięcie (również zamknięcie leksykalne lub zamknięcie funkcji) jest funkcją lub odniesieniem do funkcji wraz ze środowiskiem odniesienia - tabelą przechowującą odniesienie do każdej zmiennej nielokalnej (zwanej także zmienną swobodną) tej funkcji . Zamknięcie - w przeciwieństwie do zwykłego wskaźnika funkcji - umożliwia funkcji dostęp do tych zmiennych nielokalnych, nawet jeśli zostanie wywołana poza jej bezpośrednim zakresem leksykalnym.

Próba twojego znajomego wyraźnie wykorzystuje zmienną i, która jest nielokalna, biorąc jej wartość i wykonując kopię do zapisania w lokalnej i2.

Twoja własna próba ijest przekazywana jako argument do anonimowej funkcji (która znajduje się w witrynie wywoływania). Jak dotąd nie jest to zamknięcie, ale wtedy ta funkcja zwraca inną funkcję, która odwołuje się do tego samego i2. Ponieważ wewnętrzna funkcja anonimowa i2nie jest lokalna, powoduje to zamknięcie.


Tak, ale myślę, że chodzi o to, jak on to robi. Po prostu kopiuje ido i2, a następnie definiuje logikę i wykonuje tę funkcję. Gdybym nie wykonał go natychmiast, ale zapisałbym go w var i wykonał po pętli, wypisałby 10, prawda? Więc nie uchwycił ja.
leem

6
@leemes: Zostało idobrze zrobione . Zachowanie, które opisujesz, nie jest wynikiem zamknięcia a nie zamknięcia; jest to wynik zmiany w międzyczasie zmiennej zamkniętej. Robisz to samo, używając innej składni, natychmiast wywołując funkcję i przekazując ijako argument (który natychmiast kopiuje jej bieżącą wartość). Jeśli umieścisz własny setTimeoutw innym, stanie się setTimeoutto samo.
Jon

13

Ty i twój przyjaciel używacie zamknięć:

Zamknięcie jest szczególnym rodzajem obiektu, który łączy dwie rzeczy: funkcję i środowisko, w którym funkcja została utworzona. Środowisko składa się z dowolnych zmiennych lokalnych, które były w zasięgu w momencie tworzenia zamknięcia.

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

W kodzie twojego przyjaciela funkcja function(){ console.log(i2); }zdefiniowana w zamknięciu funkcji anonimowej function(){ var i2 = i; ...i może odczytywać / zapisywać zmienną lokalną i2.

W twoim kodzie funkcja function(){ console.log(i2); }zdefiniowana w zamknięciu funkcji function(i2){ return ...i może odczytywać / zapisywać wartości lokalne i2(zadeklarowane w tym przypadku jako parametr).

W obu przypadkach funkcja function(){ console.log(i2); }przechodzi do setTimeout.

Innym równoważnym (ale przy mniejszym zużyciu pamięci) jest:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}

1
Nie rozumiem, dlaczego twoje rozwiązanie w porównaniu z rozwiązaniem mojego przyjaciela „jest szybsze i przy mniejszym zużyciu pamięci”, czy mógłbyś to rozwinąć?
brillout,

3
W swoim rozwiązaniu tworzysz 20 obiektów funkcyjnych (2 obiekty w każdej pętli: 2x10 = 20). Taki sam wynik w rozwiązaniu twojego znajomego. W „moim” rozwiązaniu powstaje tylko 11 obiektów funkcyjnych: 1 przed pętlą i 10 „wewnątrz” - 1 + 1x10 = 11. W rezultacie - mniejsze zużycie pamięci i zwiększenie prędkości.
Andrew D.

1
Teoretycznie byłoby to prawdą. W praktyce również: Zobacz ten test porównawczy JSPerf
Rob W

10

Zamknięcie

Zamknięcie nie jest funkcją ani wyrażeniem. Musi być postrzegany jako rodzaj „migawki” ze zmiennych używanych poza zakresem funkcji i używanych wewnątrz funkcji. Gramatycznie należy powiedzieć: „zamknij zmienne”.

Innymi słowy: zamknięcie jest kopią odpowiedniego kontekstu zmiennych, od których zależy funkcja.

Jeszcze raz (naïf): Zamknięcie ma dostęp do zmiennych, które nie są przekazywane jako parametr.

Pamiętaj, że te koncepcje funkcjonalne silnie zależą od używanego języka programowania / środowiska. W JavaScript zamknięcie zależy od zakresu leksykalnego (co jest prawdą w większości języków c).

Tak więc, zwracanie funkcji polega głównie na zwróceniu funkcji anonimowej / nienazwanej. Gdy zmienna dostępu do funkcji, nieprzekazana jako parametr, i w jej (leksykalnym) zakresie, została zamknięta.

A więc jeśli chodzi o twoje przykłady:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

Wszyscy używają zamknięć. Nie myl punktu wykonania z zamknięciami. Jeśli „migawka” zamknięć zostanie wykonana w niewłaściwym momencie, wartości mogą być nieoczekiwane, ale na pewno zostanie wykonane zamknięcie!



10

Spójrzmy na oba sposoby:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

Deklaruje i natychmiast wykonuje anonimową funkcję działającą setTimeout()we własnym kontekście. Bieżąca wartość ijest zachowywana przez utworzenie kopii w i2pierwszej; działa z powodu natychmiastowego wykonania.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

Deklaruje kontekst wykonania funkcji wewnętrznej, w której izachowana jest bieżąca wartość parametru i2; to podejście wykorzystuje również natychmiastowe wykonanie w celu zachowania wartości.

Ważny

Należy wspomnieć, że semantyka uruchamiania NIE jest taka sama między obydwoma podejściami; twoja funkcja wewnętrzna zostaje przekazana, setTimeout()podczas gdy jego funkcja wewnętrzna nazywa się setTimeout()sobą.

Zawinięcie obu kodów w innym setTimeout()nie świadczy o tym, że tylko drugie podejście wykorzystuje zamknięcia, po prostu nie jest to samo na początek.

Wniosek

Obie metody wykorzystują zamknięcia, więc sprowadza się to do osobistego gustu; drugie podejście jest łatwiejsze do „przemieszczenia się” lub uogólnienia.


Myślę, że różnica polega na tym, że jego rozwiązanie (1.) jest przechwytywane przez odniesienie, moje (2.) jest przechwytywane według wartości. W tym przypadku nie robi to różnicy, ale gdybym umieścił wykonanie w innym setTimeout, zobaczylibyśmy, że jego rozwiązanie ma problem polegający na tym, że następnie używa końcowej wartości i, a nie prądu, podczas gdy mój parapet używa bieżąca wartość (od momentu przechwycenia przez wartość).
leemes

@leemes Oboje robicie w ten sam sposób; przekazywanie zmiennej za pomocą argumentu funkcji lub przypisania to to samo ... czy możesz dodać do pytania, w jaki sposób chcesz zawrzeć wykonanie w innym setTimeout()?
Ja͢ck

pozwól mi to sprawdzić ... Chciałem pokazać, że obiekt funkcji może być przekazywany, a oryginalna zmienna imoże być zmieniana bez wpływu na to, co funkcja ma wydrukować, niezależnie od tego, gdzie i kiedy ją wykonamy.
leem

Poczekaj, nie przekazałeś funkcji do (zewnętrznego) setTimeout. Usuń je (), przekazując w ten sposób funkcję, a zobaczysz 10 razy wynik 10.
leem

@leemes Jak wspomniano wcześniej, ()właśnie to sprawia, że ​​jego kod działa, podobnie jak twój (i); nie tylko otoczyłeś jego kod, wprowadziłeś do niego zmiany ... dlatego nie możesz już dokonywać prawidłowego porównania.
Ja͢ck

8

Napisałem to jakiś czas temu, aby przypomnieć sobie, czym jest zamknięcie i jak działa w JS.

Zamknięcie to funkcja, która po wywołaniu używa zakresu, w którym została zadeklarowana, a nie zakresu, w którym została wywołana. W javaScript wszystkie funkcje zachowują się w ten sposób. Zmienne wartości w zakresie pozostają tak długo, jak długo istnieje funkcja, która na nie wskazuje. Wyjątkiem od reguły jest „to”, które odnosi się do obiektu, w którym znajduje się funkcja, gdy zostanie wywołana.

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 

6

Po dokładnym zbadaniu wygląda na to, że oboje korzystacie z zamknięcia.

W twoim przypadku ijest dostępny w anonimowej funkcji 1 i i2jest dostępny w anonimowej funkcji 2, gdzie console.logjest obecny.

W twoim przypadku uzyskujesz dostęp i2do anonimowej funkcji, jeśli console.logjest obecna. Dodaj debugger;instrukcję przed console.logi w narzędziach dla programistów chrome pod „Zakres zmiennych” powie, pod jakim zakresem jest zmienna.


2
Sekcja „Zamknięcie” w prawym panelu jest używana, ponieważ nie ma już konkretnej nazwy. „Lokalny” jest silniejszym wskazaniem niż „Zamknięcie”.
Rob W


4

Rozważ następujące. To tworzy i odtwarza funkcję, fktóra się zamyka i, ale różne !:

i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

podczas gdy poniższy zamyka sam „funkcję”
(same! fragment po tym używa pojedynczego odwołania f)

for(var i = 0; i < 10; i++) {
    setTimeout( new Function('console.log('+i+')'),  1000 );
}

lub ściślej:

for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}

NB ostatnia definicja fIs function(){ console.log(9) } zanim 0 zostanie wydrukowany.

Zastrzeżenie! Koncepcja zamknięcia może być przymusowym odwróceniem uwagi od istoty programowania elementarnego:

for(var i = 0; i < 10; i++) {     setTimeout( 'console.log('+i+')',  1000 );      }

x-refs .:
Jak działają zamknięcia JavaScript?
Zamknięcia JavaScript Wyjaśnienie
Czy zamknięcie (JS) wymaga funkcji wewnątrz funkcji
Jak rozumieć zamknięcia w JavaScript?
JavaScript zamieszanie zmiennych lokalnych i globalnych


fragmenty próbowane po raz pierwszy - nie wiem, jak kontrolować - Run' only was desired - not sure how to remove the Kopiuj
ekim

-1

Chciałbym podzielić się moim przykładem i wyjaśnieniem na temat zamknięć. Zrobiłem przykład w Pythonie i dwie cyfry, aby zademonstrować stany.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n * margin_top, a * n, 
            ' ‘ * padding, msg, '  * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)

f('hello')
g(‘good bye!')

Dane wyjściowe tego kodu byłyby następujące:

*****      hello      #####

      good bye!    ♥♥♥

Oto dwie figury przedstawiające stosy i zamknięcie przymocowane do obiektu funkcyjnego.

gdy funkcja jest zwracana z programu maker

gdy funkcja zostanie wywołana później

Gdy funkcja jest wywoływana przez parametr lub zmienną nielokalną, kod potrzebuje lokalnych powiązań zmiennych, takich jak margin_top, padding oraz a, b, n. Aby zapewnić funkcjonowanie kodu funkcji, ramka stosu funkcji producenta, która już dawno zniknęła, powinna być dostępna, co jest podtrzymywane w zamknięciu, które możemy znaleźć wraz z obiektem komunikatu funkcji.


Chciałbym usunąć tę odpowiedź. Zdałem sobie sprawę, że pytanie nie dotyczy tego, co jest zamknięciem, dlatego chciałbym przenieść je na inne pytanie.
Eunjung Lee

2
Wierzę, że masz możliwość usuwania własnych treści. Kliknij deletelink pod odpowiedzią.
Rory McCrossan,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.