JavaScript kapanışları ve anonim işlevler


562

Bir arkadaşım ve ben şu anda JS'de neyin kapatıldığını ve neyin olmadığını tartışıyoruz. Sadece gerçekten doğru anladığımızdan emin olmak istiyoruz.

Bu örneği ele alalım. Bir sayma döngümüz var ve sayaç değişkenini konsolda gecikmeli olarak yazdırmak istiyoruz. Bu nedenle kullandığımız setTimeoutve kapanışları değerin N. N kez yazdırmak olmayacağından emin olmak için sayaç değişkeninin değerini yakalamak için

Olmadan yanlış bir çözümü kapanışları veya yakın bir şey kapanışları olacaktır:

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

tabii ki idöngüden sonraki değerin 10 katı , yani 10 baskı olacaktır .

Yani onun girişimi:

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

beklendiği gibi 0-9 arası yazdırma.

Ona yakalamak için bir kapak kullanmadığını söyledim i, ama ısrar ediyor. For döngüsü gövdesini bir başkasının içine koyarak (anonim işlevini geçirerek ), 10 kez 10 kez tekrar yazarak kapaklarını kullanmadığını kanıtladım . Ben onun fonksiyon kaydetmek durumunda da geçerlidir ve çalıştırmak sonra benim argüman olduğunu Yani ayrıca 10 kez 10. baskı, döngü o gerçekten değil yakalamak değerini onun versiyonunu yapma değil bir kapatma.setTimeoutsetTimeoutvari

Girişim:

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

Ben yakalama Böylece i(adlandırılmış i2kapatılması içinde), ama şimdi dönmek başka bir fonksiyon ve Bunu tersine geçmektedir. Benim durumumda, setTimeout'a iletilen işlev gerçekten yakalar i.

Şimdi kim kapanış kullanıyor, kim değil?

Her iki çözümün de 0'dan 9'a konsolda yazdırıldığını, bu nedenle orijinal sorunu çözdüklerini unutmayın, ancak bu iki çözümden hangisinin bunu gerçekleştirmek için kapaklar kullandığını anlamak istiyoruz .



1
@ leemes: İkinci bir bağlantı için ninja düzenlememe bakın.
Blender

2
Biz sadece bir anlaşma yaptık: doğru olanı bu soru ile ilgili SO puanları alacak
brillout

1
@ leemes - Her ikiniz de kapak kullanıyorsunuz. İkiniz de iki fonksiyon yaptınız - bir dış fonksiyon ve bir iç fonksiyon; ve her iki iç fonksiyonunuz da kapaklardır. Tüm işlevleriniz lambdas ( anonim işlevler ). Ayrıntılar için cevabımı okuyun.
Aadit M Shah

1
@blesh - Değiştirilmiş bir kapağın ne olduğu hakkında hiçbir fikrim yok. Bağlantınızın C # kodunu gösterdiğini görüyorum. Değiştirilmiş kapaklar JavaScript tarafından destekleniyor mu?
Aadit M Shah

Yanıtlar:


650

Editörün Notu: Bu açıklandığı gibi JavaScript tüm işlevler muhafazalardır yazı . Bununla birlikte, sadece teorik bir bakış açısından ilginç olan bu fonksiyonların bir alt kümesini tanımlamakla ilgileniyoruz . Bundan böyle kapatma kelimesine yapılan herhangi bir atıf, aksi belirtilmedikçe bu fonksiyon alt kümesine değinecektir.

Kapaklar için basit bir açıklama:

  1. Bir işlev al. Buna F diyelim.
  2. F değişkenlerinin tümünü listeleyin.
  3. Değişkenler iki tip olabilir:
    1. Yerel değişkenler (bağlı değişkenler)
    2. Yerel olmayan değişkenler (serbest değişkenler)
  4. F'nin serbest değişkeni yoksa, bir kapatma olamaz.
  5. F (tanımlanır herhangi bir serbest değişkenleri varsa bir Daha sonra F üst etki):
    1. Hangi F yalnızca bir üst kapsam olmalıdır bir serbest değişken bağlıdır.
    2. F, bu üst kapsamın dışından başvurulursa , o serbest değişken için bir kapanış haline gelir .
    3. Bu serbest değişkene F kapağının bir yükselmesi denir.

Şimdi bunu kimin kapanışları kullandığını ve kimin kullanmadığını anlamak için kullanalım (açıklama uğruna işlevleri adlandırdım):

Durum 1: Arkadaşınızın Programı

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

Yukarıdaki programda iki işlev vardır: fve g. Bakalım bunlar kapanıyor mu?

Şunun için f:

  1. Değişkenleri listeleyin:
    1. i2Bir olan yerel değişkeni.
    2. iBir olan serbest değişkeni.
    3. setTimeoutBir olan serbest değişkeni.
    4. gBir olan yerel değişkeni.
    5. consoleBir olan serbest değişkeni.
  2. Her bir serbest değişkenin bağlı olduğu ana kapsamı bulun:
    1. iolduğu bağlı küresel kapsama.
    2. setTimeoutolduğu bağlı küresel kapsama.
    3. consoleolduğu bağlı küresel kapsama.
  3. İşlev hangi kapsamda başvurulur ? Küresel kapsamı .
    1. Bu nedenle ideğildir üzerinde kapalı göre f.
    2. Bu nedenle setTimeoutdeğildir üzerinde kapalı göre f.
    3. Bu nedenle consoledeğildir üzerinde kapalı göre f.

Böylece fonksiyon fbir kapanış değildir.

Şunun için g:

  1. Değişkenleri listeleyin:
    1. consoleBir olan serbest değişkeni.
    2. i2Bir olan serbest değişkeni.
  2. Her bir serbest değişkenin bağlı olduğu ana kapsamı bulun:
    1. consoleolduğu bağlı küresel kapsama.
    2. i2bir bağlanmış kapsamına f.
  3. İşlev hangi kapsamda başvurulur ? KapsamısetTimeout .
    1. Bu nedenle consoledeğildir üzerinde kapalı göre g.
    2. Bu nedenle i2bir fazla kapalı göre g.

Bu nedenle fonksiyonu gserbest değişken için bir kapaktır i2(bir upvalue olan g) zaman bu kadar referans içinden setTimeout.

Senin için kötü: Arkadaşın bir kapak kullanıyor. İç fonksiyon bir kapaktır.

Durum 2: Programınız

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

Yukarıdaki programda iki işlev vardır: fve g. Bakalım bunlar kapanıyor mu?

Şunun için f:

  1. Değişkenleri listeleyin:
    1. i2Bir olan yerel değişkeni.
    2. gBir olan yerel değişkeni.
    3. consoleBir olan serbest değişkeni.
  2. Her bir serbest değişkenin bağlı olduğu ana kapsamı bulun:
    1. consoleolduğu bağlı küresel kapsama.
  3. İşlev hangi kapsamda başvurulur ? Küresel kapsamı .
    1. Bu nedenle consoledeğildir üzerinde kapalı göre f.

Böylece fonksiyon fbir kapanış değildir.

Şunun için g:

  1. Değişkenleri listeleyin:
    1. consoleBir olan serbest değişkeni.
    2. i2Bir olan serbest değişkeni.
  2. Her bir serbest değişkenin bağlı olduğu ana kapsamı bulun:
    1. consoleolduğu bağlı küresel kapsama.
    2. i2bir bağlanmış kapsamına f.
  3. İşlev hangi kapsamda başvurulur ? KapsamısetTimeout .
    1. Bu nedenle consoledeğildir üzerinde kapalı göre g.
    2. Bu nedenle i2bir fazla kapalı göre g.

Bu nedenle fonksiyonu gserbest değişken için bir kapaktır i2(bir upvalue olan g) zaman bu kadar referans içinden setTimeout.

Senin için iyi: Bir kapak kullanıyorsun. İç fonksiyon bir kapaktır.

Yani siz ve arkadaşınız kapak kullanıyorsunuz. Tartışmayı bırak. Umarım kapanış kavramını ve ikiniz için bunları nasıl tanımlayacağımı açıkladım.

Düzenleme: Neden tüm fonksiyonlar kapanır (krediler @Peter) basit bir açıklama:

İlk önce aşağıdaki programı ele alalım ( kontrol ):

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. Biz de biliyoruz lexicalScopeve regularFunctionkapanışları olmayan Yukarıdaki tanımdan .
  2. Biz programı çalıştırmak zaman beklediğimiz message uyarılmak için çünkü regularFunction bir kapatma değil (yani o erişimi olan tüm - dahil üst kapsamındaki değişkenler message).
  3. Biz programı çalıştırmak zaman biz gözlemlemek o messagegerçekten uyarılır.

Sonra aşağıdaki programı ele alalım ( alternatif budur ):

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. Sadece yukarıdaki tanımdanclosureFunction bir kapatma olduğunu biliyoruz .
  2. Biz programı çalıştırmak zaman beklediğimiz message uyarı almak değil , çünkü closureFunction (yani sadece tüm erişimi olan bir kapaktır yerel olmayan değişkenler de işlev oluşturulduğunda zaman ( bu cevaba bakınız - bu içermez) message).
  3. Biz programı çalıştırmak zaman biz gözlemlemek o messageaslında uyarı ediliyor.

Bundan ne çıkarıyoruz?

  1. JavaScript tercümanları, kapanışlara diğer işlevlere göre farklı davranmazlar.
  2. Her fonksiyon, kapsam zincirini onunla birlikte taşır . Kapakların ayrı bir referans ortamı yoktur.
  3. Kapanış diğer tüm işlevler gibidir. Biz sadece onlar ne zaman onları kapanışları çağrı başvurulan bir kapsamda dışında , ait oldukları için kapsam çünkü bu ilginç bir durumdur.

40
Kabul edildi çünkü ayrıntılara çok gidiyorsunuz, neler olduğunu çok güzel açıklıyor. Ve son olarak, şimdi bir kapağın ne olduğunu daha iyi anladım ya da daha iyi bir şey söyledim: JS'de değişken bağlamanın nasıl çalıştığını.
leemes

3
Durum 1'de gbunun kapsamı içinde setTimeoutolduğunu söylersiniz, ancak Durum 2'de fbunun global kapsamda olduğunu söylersiniz . İkisi de setTimeout içinde, bu yüzden fark nedir?
rosscj2533

9
Bunun için kaynaklarınızı belirtir misiniz? Bir işlevin bir kapsamda çağrılırsa, ancak başka bir kapsamda çağrılmazsa, bir fonksiyonun nerede olabileceği konusunda bir tanım görmedim. Bu nedenle, bu tanım , bir kapağın bir kapatma olduğu bir yerde daha genel tanımın bir alt kümesi gibi görünür ( kev'nin cevabına bakın ), adı verilen kapsamdan bağımsız olarak veya hiç çağrılmasa bile bir kapatmadır!
Briguy37

11
@AaditMShah Bir kapağın ne olduğu konusunda size katılıyorum, ancak JavaScript'teki normal işlevler ve kapanmalar arasında bir fark varmış gibi konuşuyorsunuz . Fark yok; dahili olarak her fonksiyon beraberinde yaratıldığı belirli kapsam zincirine bir referans taşıyacaktır. JS motoru farklı bir durum olarak düşünmüyor. Karmaşık bir kontrol listesine gerek yoktur; sadece her fonksiyon nesnesinin sözcüksel kapsam taşıdığını bilin. Değişkenlerin / özelliklerin küresel olarak kullanılabilir olması, işlevi daha az bir kapanış yapmaz (sadece işe yaramaz bir durumdur).
Peter

13
@Peter - Biliyor musun, haklısın. Normal bir işlev ile bir kapatma arasında hiçbir fark yoktur. Bunu kanıtlamak için bir test yaptım ve bu sizin lehinize sonuçlandı: İşte kontrol ve işte alternatif . Söylediklerin mantıklı. JavaScript yorumlayıcısının kapanışlar için özel defter tutma ihtiyacı vardır. Bunlar, birinci sınıf işlevlere sahip, sözcüksel olarak kapsamlı bir dilin yan ürünleridir. Bilgim okuduğum şeyle sınırlıydı (ki bu yanlıştı). Beni doğruladığınız için teşekkür ederim. Cevabımı aynısını yansıtacak şekilde güncelleyeceğim.
Aadit M Shah

96

closureTanıma göre :

"Kapatma", bu değişkenleri bağlayan (ifadeyi "kapatan") bir ortamla birlikte serbest değişkenlere sahip olabilen bir ifadedir (genellikle bir işlevdir ).

closureİşlev dışında tanımlanan bir değişkeni kullanan bir işlev tanımlarsanız kullanıyorsunuzdur . (değişkene serbest değişken diyoruz ).
Hepsi kullanılır closure(1. örnekte bile).


1
Üçüncü sürüm fonksiyonun dışında tanımlanan bir değişkeni nasıl kullanır?
Jon

1
@ i2Dışında tanımlanan döndürülen işlevi kullanın .
kev

1
@kev İşlevin dışında tanımlanan bir değişken kullanan bir işlev tanımlarsanız kapatma kullanıyorsunuz ...... sonra "Aadit M Shah" yanıtının "Durum 1: Arkadaşınızın Programı" nda "işlev f" bir kapatma? i (fonksiyonun dışında tanımlanan değişken) kullanır. küresel kapsam belirleyiciye referansta bulunuyor mu?
internals-in


54

Özetle JavaScript Closures bir fonksiyonu sağlayan erişim bir değişkeni olan bir sözcük ebeveynli fonksiyonunda ilan .

Daha ayrıntılı bir açıklama görelim. Kapanışları anlamak için JavaScript'in değişkenleri nasıl kapsadığını anlamak önemlidir.

kapsamları

JavaScript'te kapsamlar işlevlerle tanımlanır. Her işlev yeni bir kapsam tanımlar.

Aşağıdaki örneği ele alalım;

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

f baskı çağrı

hello
hello
2
Am I Accessible?

Şimdi gbaşka bir fonksiyonda tanımlanmış bir fonksiyonumuz olduğunu düşünelim f.

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

Biz arayacak sözcük ebeveyne ait . Daha önce açıklandığı gibi, şimdi 2 kapsamımız var; kapsam ve kapsam .fgfg

Ancak bir kapsam diğer kapsamda "kapsamdadır", bu nedenle child fonksiyonunun kapsamı, ana fonksiyonun kapsamının bir parçası mıdır? Üst işlev kapsamında açıklanan değişkenlerle ne olur; bunlara çocuk işlevi kapsamından erişebilecek miyim? Tam olarak kapanışlar devreye giriyor.

Kapaklar

JavaScript'te işlev gyalnızca kapsamda bildirilen değişkenlere gdeğil, üst işlev kapsamında bildirilen değişkenlere de erişebilir f.

Aşağıdakileri düşünün;

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

f baskı çağrı

hello
undefined

Çizgiye bakalım console.log(foo); . Bu noktada kapsamdayız gve fookapsamda bildirilen değişkene erişmeye çalışıyoruz f. Ancak daha önce de belirtildiği gibi, burada söz konusu olan sözcüksel bir üst işlevde bildirilen herhangi bir değişkene erişebiliriz; gsözlü ebeveynidir f. Bu nedenle helloyazdırılır.
Şimdi çizgiye bakalım console.log(bar);. Bu noktada kapsamdayız fve barkapsamda bildirilen değişkene erişmeye çalışıyoruz g. bargeçerli kapsamda bildirilmez ve işlev güst öğesi değildir f, bu nedenle bartanımsızdır

Aslında, sözcüksel "büyük ebeveyn" işlevi kapsamında bildirilen değişkenlere de erişebiliriz. Bu nedenle h, işlev içinde tanımlanmış bir işlev varsag

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

Daha sonra hfonksiyonun kapsamda bildirilen tüm değişkenleri erişmek mümkün olacağını h, gve f. Bu kapaklarla yapılır . JavaScript içinde kapakları Bu olarak görülebilir vb sözcük büyük-bin ana fonksiyonu, içinde, sözcük büyük ana işlevi, sözcük üst işlevi bildirilen herhangi bir değişken erişmek olanak sağlıyor kapsam zinciri ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... sözcüksel ebeveyni olmayan son ana işleve kadar.

Pencere nesnesi

Aslında zincir son ana işlevde durmaz. Bir tane daha özel kapsam var; Küresel kapsamı . Bir işlevde bildirilmeyen her değişkenin küresel kapsamda bildirildiği kabul edilir. Küresel kapsam iki özelliğe sahiptir;

  • Genel kapsamda bildirilen her değişken erişilebilir her yerde
  • genel kapsamda bildirilen değişkenler windownesnenin özelliklerine karşılık gelir .

Bu nedenle foo, küresel kapsamda bir değişken bildirmenin tam olarak iki yolu vardır ; ya bir işlevde bildirmeyerek ya fooda pencere nesnesinin özelliğini ayarlayarak .

Her iki deneme de kapanışları kullanıyor

Şimdi daha ayrıntılı bir açıklama okuduğunuza göre, artık her iki çözümün de kapanışları kullandığı görülebilir. Ama emin olmak için bir kanıt yapalım.

Yeni bir Programlama Dili oluşturalım; JavaScript No-Kapanış. Adından da anlaşılacağı gibi, JavaScript-No-Closure, Kapanışları desteklemediği için JavaScript ile aynıdır.

Başka bir deyişle;

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

Tamam, JavaScript-No-Closure ile ilk çözümde neler olduğunu görelim;

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

bu nedenle bu yazdırılacaktır undefined , JavaScript-No-Closure'da 10 kez .

Bu nedenle ilk çözüm kapama kullanır.

İkinci çözüme bakalım;

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

bu nedenle bu yazdırılacaktır undefined , JavaScript-No-Closure'da 10 kez .

Her iki çözüm de kapanış kullanır.

Düzenleme: Bu 3 kod parçacıklarının genel kapsamda tanımlanmadığı varsayılır. Aksi takdirde, değişkenler foove nesneye ibağlanır windowve bu nedenle windowhem JavaScript hem de JavaScript-No-Closure içindeki nesne aracılığıyla erişilebilir .


Neden itanımsız olmalı ? Sadece hiçbir kapatma olmadığında geçerli olan ana kapsama bakın.
leemes

JavaScript-No-Closure'da foo ile aynı nedenden dolayı tanımsızdır. <code> i </code>, JavaScript'teki sözlüksel üst öğede tanımlanan değişkenlere erişmeyi sağlayan bir özellik sayesinde JavaScript'te tanımlanmamıştır. Bu özelliğe kapatma adı verilir.
brillout

Önceden tanımlanmış değişkenlere ve serbest değişkenlere gönderme arasındaki farkı anlamadınız . Kapanışlarda, dış bağlamda bağlanması gereken serbest değişkenleri tanımlarız. Kodunuzda, sadece set i2 için isize işlevini tanımlarken anda. Bu iserbest değişken DEĞİLDİR. Yine de, işlevinizi bir kapatma olarak görüyoruz, ancak herhangi bir serbest değişken olmadan, mesele bu.
leemes

2
@ leemes, katılıyorum. Ve kabul edilen cevaba kıyasla, bu gerçekten neler olup bittiğini göstermiyor. :)
Abel

3
bence bu en iyi cevap, kapanışları genel ve basit bir şekilde açıklayan ve daha sonra özel kullanım durumuna geçti. Teşekkürler!
tim peterson

22

Hiç kimsenin bunu açıklama biçiminden hiç memnun kalmadım.

Kapakları anlamanın anahtarı JS'nin kapaklar olmadan nasıl olacağını anlamaktır.

Kapaklar olmadan bu bir hata verir

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

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

OuterFunc, JavaScript'in hayali bir kapanma devre dışı bırakılmış sürümünde döndüğünde, outerVar'a yapılan başvuru çöp toplanır ve iç işlevin başvurması için hiçbir şey bırakmaz.

Kapanışlar esasen, bir iç fonksiyon bir dış fonksiyonun değişkenlerine başvurduğunda bu değişkenlerin varlığını mümkün kılan özel kurallardır. Kapaklarda başvurulan değişkenler, dış fonksiyon yapıldıktan sonra bile korunur veya noktayı hatırlamanıza yardımcı olursa 'kapatılır'.

Kapanışlarda bile, yerelin yaşam döngüsü, yerellerine referans veren iç işlevlere sahip olmayan bir işlevde değişir, kapanmasız bir versiyonda olduğu gibi çalışır. İşlev tamamlandığında, yerliler çöp toplar.

Bir iç fonkta bir dış değişkene referansınız olduğunda, ancak referans verilen bu variller için çöp toplama yoluna konan bir kapı sövüğü gibi.

Kapaklara bakmak için belki de daha doğru bir yol, iç fonksiyonun temel olarak iç kapsamı kendi kapsam foudnasyonu olarak kullanmasıdır.

Ancak, atıfta bulunulan bağlam, gerçekte kalıcıdır, anlık görüntü gibi değildir. Bir dış işlevin yerel değişkenini artırmaya ve günlüğe kaydetmeye devam eden döndürülen bir iç işlevi tekrar tekrar ateşlemek, daha yüksek değerleri uyarmaya devam edecektir.

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

Bununla ilgili 'anlık görüntü' konusunda haklısın (sanırım, cevabımdan bahsediyorsun). Davranışı belirten bir kelime arıyordum. Örneğinizde, bir 'hotlink' kapatma yapısı olarak görülebilir. Kapama iç fonksiyonda parametre olarak yakalanırken, bir 'anlık görüntü' olarak davrandığı söylenebilir. Ama katılıyorum, yanlış kullanılan kelimeler sadece konuya karışıklık katıyor. Bununla ilgili herhangi bir öneriniz varsa, cevabımı güncelleyeceğim.
Andries

İç fonksiyonun adlandırılmış bir fonksiyon olduğunu açıklamak size yardımcı olabilir.
Phillip Senn

Kapatma olmadan, var olmayan bir değişkeni kullanmaya çalıştığınız için bir hata alırsınız.
Juan Mendes

Hmm ... güzel nokta. Tanımlanamayan bir değişkene başvurmak, nihayetinde global nesne üzerinde bir özellik olarak görüneceği için hiç hata atmadı mı yoksa tanımsız değişkenlere atama ile karıştırıyorum mu?
Erik Reppen

17

İkiniz de kapak kullanıyorsunuz.

Burada Wikipedia tanımıyla gidiyorum :

Bilgisayar bilimlerinde, bir kapatma (aynı zamanda sözcüksel kapatma veya işlev kapatma), bir referans ortamıyla birlikte bir işleve veya bir işleve başvurudır - bu işlevin yerel olmayan değişkenlerinin (serbest değişkenler olarak da adlandırılır) her birine bir referans depolayan bir tablodur . Kapatma - düz işlev işaretçisinden farklı olarak - bir işlevin anlık sözcük kapsamı dışında çağrıldığında bile yerel olmayan değişkenlere erişmesini sağlar.

Arkadaşınızın girişimi i, değerini alma ve yerelde saklamak için bir kopya oluşturarak yerel olmayan değişkeni açıkça kullanır i2.

Kendi girişimleriniz i(çağrı sitesinde yer alan) argüman olarak anonim bir işleve geçer. Bu, şu ana kadar bir kapatma değil, ancak bu işlev aynı referansı veren başka bir işlev döndürüyor i2. İç anonim işlev içinde i2bir yerel olmadığından, bu bir kapatma oluşturur.


Evet, ama bence mesele bunu nasıl yapıyor. Sadece kopya O ikadar i2, sonra bazı mantık tanımlar ve bu işlevi yürütür. Ben hemen yürütmezdim , ama bir var saklayın ve döngüden sonra çalıştırdı, 10 yazdı, değil mi? Bu yüzden mi değil i yakalamak.
leemes

6
@ leemes: Çok iyi yakalandı i. Tanımladığınız davranış, kapatma ile kapanmamasının bir sonucu değildir; bu arada kapalı değişkenin değişmesinin bir sonucudur. Aynı işlevi, hemen bir işlevi çağırarak ve ibağımsız değişken olarak ileterek (geçerli değerini yerinde kopyalayan) farklı sözdizimi kullanarak yapıyorsunuz . Eğer kendi setTimeoutiçine bir başkasını koyarsan setTimeoutaynı şey olur.
Jon

13

Siz ve arkadaşınız kapanışları kullanıyorsunuz:

Kapatma, iki şeyi birleştiren özel bir nesne türüdür: bir işlev ve bu işlevin oluşturulduğu ortam. Ortam, kapatmanın oluşturulduğu sırada kapsam dahilindeki tüm yerel değişkenlerden oluşur.

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

Arkadaşınızın kod fonksiyonu function(){ console.log(i2); }anonim fonksiyonu kapanış içinde tanımlanan function(){ var i2 = i; ...ve yerel değişken okuyabilir / yazabilirsinizi2 .

Kodunuzda fonksiyonun function(){ console.log(i2); }kapatılması içinde tanımlanmış function(i2){ return ...ve yerel değerli okuma / yazmai2 (bu durumda parametre olarak bildirilir).

Her iki durumda da fonksiyon function(){ console.log(i2); }daha sonrasetTimeout .

Başka bir eşdeğer (ancak daha az bellek kullanımı ile):

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

1
Arkadaşınızın çözümüne karşı çözümünüzün neden "daha hızlı ve daha az bellek kullanımı" olduğunu anlamıyorum, biraz daha açıklayabilir misiniz?
brillout

3
Çözümünüzde 20 işlev nesnesi oluşturursunuz (her döngüde 2 nesne: 2x10 = 20). Aynı sonuç arkadaşınızın çözümünde. "My" çözümünde sadece 11 fonksiyon nesnesi oluşturulur: 1 önce loop için ve 10 "inside" - 1 + 1x10 = 11. Sonuç olarak - daha az bellek kullanımı ve hız artışı.
Andrew D.17

1
Teorik olarak, bu doğru olurdu. Uygulamada ayrıca: Bu JSPerf karşılaştırmalı değerlendirmesine bakın: jsperf.com/closure-vs-name-function-in-a-loop/2
Rob W

10

kapatma

Kapatma bir işlev değildir, bir ifade değildir. Functioncope dışında kullanılan değişkenlerden bir tür 'anlık görüntü' olarak görülmeli ve işlevin içinde kullanılmalıdır. Dilbilgisel olarak şunu söylemeliyiz: 'değişkenlerin kapanışını al'.

Yine başka bir deyişle: Kapatma, işlevin bağlı olduğu değişkenlerin ilgili bağlamının bir kopyasıdır.

Bir kez daha (naïf): Kapatma, parametre olarak iletilmeyen değişkenlere erişime sahip.

Bu işlevsel kavramların büyük ölçüde kullandığınız programlama diline / ortama bağlı olduğunu unutmayın. JavaScript'te kapatma, sözcüksel kapsam belirlemeye bağlıdır (çoğu c dilinde geçerlidir).

Bu nedenle, bir işlevi döndürmek çoğunlukla anonim / adsız bir işlevi döndürmektir. İşlev erişim değişkenleri parametre olarak iletilmediğinde ve (sözcüksel) kapsamı içinde bir kapatma işlemi gerçekleştirildi.

Örneklerinizle ilgili olarak:

// 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)
}

Hepsi kapanış kullanıyor. Uygulama noktasını kapanışlarla karıştırmayın. Kapakların 'anlık görüntüsü' yanlış anda alınırsa, değerler beklenmedik olabilir ancak kesinlikle bir kapanış alınır!



10

Her iki yöne de bakalım:

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

setTimeout()Kendi bağlamında çalışan anonim bir işlevi bildirir ve hemen yürütür . Geçerli değeri, önce ibir kopya yapılarak korunur i2; derhal idam nedeniyle çalışır.

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

İç fonksiyonu için geçerli değerinin ikorunduğu bir yürütme bağlamı bildirir i2; bu yaklaşım, değeri korumak için derhal yürütmeyi de kullanır.

Önemli

Her iki yaklaşım arasında çalışma semantiğinin aynı OLMADIĞI belirtilmelidir; içsel işleviniz geçerken setTimeout()içsel işlevi setTimeout()kendini çağırır .

Her iki kodu da diğerine sarmak setTimeout(), sadece ikinci yaklaşımın kapanış kullandığını kanıtlamaz, başlamak için aynı şey yoktur.

Sonuç

Her iki yöntem de kapak kullanır, bu nedenle kişisel zevkinize gelir; ikinci yaklaşımın "hareket etmesi" veya genelleştirilmesi daha kolaydır.


Bence fark şu: Onun çözümü (1.) referans olarak ele alıyor, benim (2.) değeri tarafından ele geçiriliyor. Bu durumda bir fark yaratmaz, ancak yürütmeyi başka bir setTimeout'a koyacak olsaydım, çözümünün, mayın eşiği kullanıldığında, akımın değil, i'nin son değerini kullanması sorununa sahip olduğunu görürüz. geçerli değer (değere göre yakalandığından beri).
leemes

@ leemes İkiniz de aynı şekilde yakalarsınız; işlev bağımsız değişkeni veya atama yoluyla bir değişkeni iletmek aynı şeydir ... sorgunuza yürütmeyi bir başkasına nasıl saracağınızı ekleyebilir misiniz setTimeout()?
Ja͢ck

Bunu kontrol edeyim ... Fonksiyon nesnesinin etrafından geçilebildiğini ve orijinal değişkenin i, nerede veya ne zaman yürüttüğümüze bağlı olarak fonksiyonun ne yazdıracağını etkilemeden değiştirilebileceğini göstermek istedim .
leemes

Bekle, setTimeout'a (dış) bir işlev iletmediniz. ()Bunları kaldırın , böylece bir işlevi geçirin ve çıktının 10 katını görürsünüz 10.
leemes

@ leemes Daha önce de belirtildiği gibi (), tam da kodunuzu çalıştıran şey tam olarak sizin gibi (i); sadece kodunu sarmadın, kodda değişiklik yaptın .. bu yüzden artık geçerli bir karşılaştırma yapamazsın.
Ja͢ck

8

Bunu bir süre önce kendime bir kapanışın ne olduğunu ve JS'de nasıl çalıştığını hatırlatmak için yazdım.

Kapatma, çağrıldığında, çağrıldığı kapsamı değil, bildirildiği kapsamı kullanan bir işlevdir. JavaScript'te, tüm işlevler bu şekilde davranır. Bir kapsamdaki değişken değerler, hala onları işaret eden bir işlev olduğu sürece devam eder. Kuralın istisnası, fonksiyon çağrıldığında içinde bulunduğu nesneyi ifade eden 'this' dir.

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

6

Yakından inceledikten sonra, her ikiniz de kapatma kullanıyor gibi görünüyor.

Arkadaşlarınızın durumunda, ianonim fonksiyon 1 içinde i2erişilir ve anonim fonksiyon 2'de erişilir.console.log mevcut .

Sizin durumunuzda , mevcut olan i2anonim fonksiyonun içinden erişiyorsunuzdur console.log. "Scope değişkenleri" altındaki krom geliştirici araçlarına debugger;önce console.logve bir deyim ekleyerek değişkenin hangi kapsamda olduğunu söyleyecektir.


2
Belirli bir ad olmadığından sağ paneldeki "Kapatma" bölümü kullanılır. "Yerel", "Kapanış" tan daha güçlü bir göstergedir.
Rob W


4

Aşağıdakileri göz önünde bulundur. Bu, fkapatılan bir işlev yaratır ve yeniden yaratır i, ancak farklı işlevler !:

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

aşağıdakiler "bir" işlev "in kendisini kapatır
(tek bir başvuru kullandıktan sonra kendileri! snippet'i f)

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

veya daha açık olmak gerekirse:

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

NB. son tanım fise function(){ console.log(9) } daha önce 0 yazdırılır.

Uyarı! Kapatma kavramı, temel programlamanın özünden zorlayıcı bir dikkat dağıtıcı olabilir:

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

x-refs .:
JavaScript kapanışları nasıl çalışır?
Javascript Kapanışları Açıklama
Bir (JS) Kapanması Bir İşlev İçinde Bir İşlev Gerektiriyor
Javascript'teki kapanışları nasıl anlayabilirim?
Javascript yerel ve global değişken karışıklığı


parçacıklar 1. kez denedi - nasıl kontrol edileceğinden emin değilim - Run' only was desired - not sure how to remove the Kopyala
ekim

-1

Örneğimi ve kapanışlarla ilgili bir açıklamayı paylaşmak istiyorum. Bir python örneği ve yığın durumlarını göstermek için iki figür yaptım.

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!')

Bu kodun çıktısı aşağıdaki gibi olacaktır:

*****      hello      #####

      good bye!    ♥♥♥

Burada fonksiyon nesnesine eklenen yığınları ve kapağı gösteren iki şekil vardır.

işlev yapıcıdan döndürüldüğünde

fonksiyon daha sonra çağrıldığında

İşlev bir parametre veya yerel olmayan bir değişken aracılığıyla çağrıldığında, kodun margin_top, dolgu ve a, b, n gibi yerel değişken bağlamaları gerekir. İşlev kodunun çalışmasını sağlamak için, uzun zaman önce kaybolan yapıcı işlevinin yığın çerçevesine erişilebilmelidir; bu, işlev mesajı nesnesiyle birlikte bulabileceğimiz kapakta yedeklenir.


Bu cevabı kaldırmak istiyorum. Sorunun kapanma ile ilgili olmadığını fark ettim, bu yüzden diğer soruya taşımak istiyorum.
Eunjung Lee

2
Kendi içeriğinizi silebileceğinize inanıyorum. deleteCevabın altındaki bağlantıyı tıklayın .
Rory McCrossan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.