Penutupan JavaScript vs. fungsi anonim

562

Seorang teman saya dan saya saat ini sedang mendiskusikan apa itu penutupan di JS dan apa yang tidak. Kami hanya ingin memastikan bahwa kami benar-benar memahaminya dengan benar.

Mari kita ambil contoh ini. Kami memiliki loop penghitungan dan ingin mencetak variabel penghitung pada konsol yang tertunda. Oleh karena itu kita gunakan setTimeoutdan penutupan untuk menangkap nilai variabel counter untuk memastikan bahwa tidak akan mencetak N kali nilai N.

Solusi yang salah tanpa penutupan atau apa pun yang dekat dengan penutupan adalah:

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

yang tentu saja akan mencetak 10 kali nilai isetelah loop, yaitu 10.

Jadi upayanya adalah:

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

mencetak 0 hingga 9 seperti yang diharapkan.

Saya mengatakan kepadanya bahwa dia tidak menggunakan penutupan untuk menangkap i, tetapi dia bersikeras bahwa dia. Saya membuktikan bahwa dia tidak menggunakan penutupan dengan meletakkan for loop body di dalam yang lain setTimeout(meneruskan fungsi anonimnya setTimeout), mencetak 10 kali 10 lagi. Hal yang sama berlaku jika saya menyimpan fungsinya dalam a vardan menjalankannya setelah loop, juga mencetak 10 kali 10. Jadi argumen saya adalah bahwa dia tidak benar-benar menangkap nilaii , membuat versinya bukan penutup.

Upaya saya adalah:

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

Jadi saya menangkap i(dinamai i2dalam penutupan), tapi sekarang saya mengembalikan fungsi lain dan menyebarkannya. Dalam kasus saya, fungsi yang diteruskan ke setTimeout benar-benar menangkap i.

Sekarang siapa yang menggunakan penutupan dan siapa yang tidak?

Perhatikan bahwa kedua solusi mencetak 0 hingga 9 pada konsol tertunda, sehingga mereka memecahkan masalah asli, tetapi kami ingin memahami yang mana dari kedua solusi yang menggunakan penutupan untuk mencapai ini.

tema
sumber
1
@ tema: Lihat edit ninja saya untuk tautan kedua.
Blender
2
kami baru saja membuat kesepakatan: orang yang benar akan mendapatkan poin SO terkait dengan pertanyaan ini
brillout
1
@ tema - Anda berdua menggunakan penutupan. Anda berdua telah membuat dua fungsi - fungsi luar dan fungsi dalam; dan kedua fungsi batin Anda adalah penutupan. Semua fungsi Anda adalah lambdas ( fungsi anonim ). Baca jawaban saya untuk detailnya.
Aadit M Shah
1
@blesh - Saya tidak tahu apa itu penutupan dimodifikasi. Saya melihat bahwa tautan Anda mengarah ke kode C #. Apakah penutupan yang dimodifikasi didukung oleh JavaScript?
Aadit M Shah

Jawaban:

650

Catatan Editor: Semua fungsi dalam JavaScript adalah penutup seperti yang dijelaskan dalam posting ini . Namun kami hanya tertarik untuk mengidentifikasi subset dari fungsi-fungsi ini yang menarik dari sudut pandang teoritis. Selanjutnya referensi ke kata closure akan merujuk pada subset fungsi ini kecuali dinyatakan lain.

Penjelasan sederhana untuk penutupan:

  1. Ambil fungsi. Sebut saja F.
  2. Daftar semua variabel F.
  3. Variabel dapat terdiri dari dua jenis:
    1. Variabel lokal (variabel terikat)
    2. Variabel non-lokal (variabel gratis)
  4. Jika F tidak memiliki variabel bebas maka itu tidak bisa menjadi penutupan.
  5. Jika F memiliki variabel bebas (yang didefinisikan dalam suatu lingkup induk F) maka:
    1. Harus ada hanya satu lingkup induk dari F yang merupakan variabel bebas terikat.
    2. Jika F dirujuk dari luar yang ruang lingkup orangtua, maka itu menjadi penutupan yang variabel bebas.
    3. Itu variabel bebas disebut upvalue dari penutupan F.

Sekarang mari kita gunakan ini untuk mencari tahu siapa yang menggunakan penutupan dan siapa yang tidak (demi penjelasan saya telah menyebutkan fungsi):

Kasus 1: Program Teman Anda

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

Dalam program di atas ada dua fungsi: fdan g. Mari kita lihat apakah itu adalah penutupan:

Untuk f:

  1. Daftar variabel:
    1. i2adalah variabel lokal .
    2. iadalah variabel gratis .
    3. setTimeoutadalah variabel gratis .
    4. gadalah variabel lokal .
    5. consoleadalah variabel gratis .
  2. Temukan cakupan induk tempat setiap variabel bebas terikat:
    1. iadalah terikat untuk lingkup global.
    2. setTimeoutadalah terikat untuk lingkup global.
    3. consoleadalah terikat untuk lingkup global.
  3. Dalam lingkup manakah fungsi itu dirujuk ? Ruang lingkup global .
    1. Karenanya itidak ditutup oleh f.
    2. Karenanya setTimeouttidak ditutup oleh f.
    3. Karenanya consoletidak ditutup oleh f.

Dengan demikian fungsinya fbukan merupakan penutupan.

Untuk g:

  1. Daftar variabel:
    1. consoleadalah variabel gratis .
    2. i2adalah variabel gratis .
  2. Temukan cakupan induk tempat setiap variabel bebas terikat:
    1. consoleadalah terikat untuk lingkup global.
    2. i2adalah terikat dengan ruang lingkup f.
  3. Dalam lingkup manakah fungsi itu dirujuk ? Ruang lingkupsetTimeout .
    1. Karenanya consoletidak ditutup oleh g.
    2. Karena i2itu ditutup oleh g.

Dengan demikian fungsi gadalah penutupan untuk variabel bebas i2(yang merupakan upvalue untuk g) saat itu dirujuk dari dalam setTimeout.

Buruk untuk Anda: Teman Anda menggunakan penutupan. Fungsi batin adalah penutup.

Kasus 2: Program Anda

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

Dalam program di atas ada dua fungsi: fdan g. Mari kita lihat apakah itu adalah penutupan:

Untuk f:

  1. Daftar variabel:
    1. i2adalah variabel lokal .
    2. gadalah variabel lokal .
    3. consoleadalah variabel gratis .
  2. Temukan cakupan induk tempat setiap variabel bebas terikat:
    1. consoleadalah terikat untuk lingkup global.
  3. Dalam lingkup manakah fungsi itu dirujuk ? Ruang lingkup global .
    1. Karenanya consoletidak ditutup oleh f.

Demikian fungsinya fbukan merupakan penutupan.

Untuk g:

  1. Daftar variabel:
    1. consoleadalah variabel gratis .
    2. i2adalah variabel gratis .
  2. Temukan cakupan induk tempat setiap variabel bebas terikat:
    1. consoleadalah terikat untuk lingkup global.
    2. i2adalah terikat dengan ruang lingkup f.
  3. Dalam lingkup manakah fungsi itu dirujuk ? Ruang lingkupsetTimeout .
    1. Karenanya consoletidak ditutup olehg.
    2. Karena i2itu ditutup oleh g.

Dengan demikian fungsi gadalah penutupan untuk variabel bebas i2(yang merupakan upvalue untuk g) saat itu dirujuk dari dalam setTimeout.

Bagus untukmu: Anda menggunakan penutupan. Fungsi batin adalah penutup.

Jadi Anda dan teman Anda menggunakan penutupan. Berhenti berdebat. Saya harap saya menjelaskan konsep penutupan dan bagaimana mengidentifikasi mereka untuk Anda berdua.

Edit: Penjelasan sederhana mengapa semua fungsi ditutup (kredit @Peter):

Pertama mari kita pertimbangkan program berikut (ini kontrolnya ):

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. Kita tahu bahwa keduanya lexicalScopedan regularFunctionbukan penutup dari definisi di atas .
  2. Saat kami menjalankan program, kami berharap message akan disiagakan karena regularFunction bukan merupakan penutupan (artinya ia memiliki akses ke semua variabel dalam cakupan induknya - termasukmessage ).
  3. Ketika kita menjalankan program kami mengamati bahwa messagememang ada peringatan.

Selanjutnya mari kita pertimbangkan program berikut (ini alternatifnya ):

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. Kami hanya tahu itu closureFunction merupakan penutup dari definisi di atas .
  2. Ketika kami menjalankan program, kami berharap message tidak akan diberitahu karena closureFunction merupakan penutupan (yaitu hanya memiliki akses ke semua variabel non-lokal pada saat fungsi dibuat ( lihat jawaban ini ) - ini tidak termasukmessage ).
  3. Ketika kami menjalankan program yang kami amati bahwa messagesebenarnya sedang disiagakan.

Apa yang kita simpulkan dari ini?

  1. Penerjemah JavaScript tidak memperlakukan penutupan secara berbeda dari cara mereka memperlakukan fungsi lain.
  2. Setiap fungsi membawa rantai cakupannya bersamanya. Penutupan tidak memiliki lingkungan referensi yang terpisah .
  3. Penutupan sama seperti fungsi lainnya. Kami hanya menyebutnya penutupan ketika direferensikan dalam lingkup di luar lingkup yang menjadi milik mereka karena ini merupakan kasus yang menarik.
Aadit M Shah
sumber
40
Diterima karena Anda sangat detail, menjelaskan apa yang sedang terjadi. Dan akhirnya, saya sekarang lebih mengerti apa itu penutupan, atau lebih baik mengatakan: bagaimana variabel mengikat bekerja di JS.
Tema
3
Dalam Kasus 1, Anda mengatakan itu gberjalan dalam cakupan setTimeout, tetapi dalam Kasus 2 Anda mengatakan itu fberjalan dalam cakupan global. Keduanya ada dalam setTimeout, jadi apa bedanya?
rosscj2533
9
Bisakah Anda sebutkan sumber Anda untuk ini? Saya belum pernah melihat definisi di mana fungsi bisa menjadi penutupan jika dipanggil dalam satu lingkup tetapi tidak di yang lain. Dengan demikian, definisi ini tampak seperti himpunan bagian dari definisi yang lebih umum yang saya gunakan (lihat jawaban Kev ) di mana penutupan adalah penutupan adalah penutupan terlepas dari ruang lingkup yang disebutnya, atau bahkan jika itu tidak pernah disebut!
Briguy37
11
@AaditMShah Saya setuju dengan Anda tentang apa penutupan itu, tetapi Anda berbicara seolah-olah ada perbedaan antara fungsi biasa dan penutupan di JavaScript. Tidak ada perbedaan; secara internal setiap fungsi akan membawa referensi ke rantai lingkup tertentu di mana ia dibuat. Mesin JS tidak menganggapnya sebagai kasus yang berbeda. Tidak perlu daftar periksa yang rumit; hanya tahu bahwa setiap objek fungsi membawa ruang lingkup leksikal. Fakta bahwa variabel / properti tersedia secara global tidak menjadikan fungsi ini kurang sebagai penutup (itu hanya kasus yang tidak berguna).
Peter
13
@ Peter - Anda tahu apa, Anda benar. Tidak ada perbedaan antara fungsi reguler dan penutupan. Saya menjalankan tes untuk membuktikan ini dan hasilnya menguntungkan Anda: inilah kendali dan inilah alternatifnya . Apa yang Anda katakan itu masuk akal. Penerjemah JavaScript perlu melakukan pembukuan khusus untuk penutupan. Mereka hanyalah produk sampingan dari bahasa yang dibatasi leksikal dengan fungsi kelas satu. Pengetahuan saya terbatas pada apa yang saya baca (yang salah). Terima kasih telah mengoreksi saya. Saya akan memperbarui jawaban saya untuk mencerminkan hal yang sama.
Aadit M Shah
96

Menurut closuredefinisi:

"Penutupan" adalah ekspresi (biasanya fungsi) yang dapat memiliki variabel gratis bersama-sama dengan lingkungan yang mengikat variabel-variabel tersebut (yang "menutup" ekspresi).

Anda menggunakan closurejika Anda mendefinisikan suatu fungsi yang menggunakan variabel yang didefinisikan di luar fungsi. (kami menyebut variabel variabel bebas ).
Mereka semua menggunakan closure(bahkan dalam contoh 1).

kev
sumber
1
Bagaimana versi ketiga menggunakan variabel yang ditentukan di luar fungsi?
Jon
1
@Jon menggunakan fungsi yang dikembalikan i2yang didefinisikan di luar.
kev
1
@ kev Anda menggunakan closure jika Anda mendefinisikan suatu fungsi yang menggunakan variabel yang didefinisikan di luar fungsi ...... maka dalam "Kasus 1: Program Teman Anda" dari "Aadit M Shah" jawabannya adalah "fungsi f" penutupan? menggunakan i (variabel yang didefinisikan di luar fungsi). apakah ruang lingkup global referensi penentu?
internals-in
54

Singkatnya Javascript Closures memungkinkan suatu fungsi untuk mengakses variabel yang dideklarasikan dalam fungsi lexical-parent .

Mari kita lihat penjelasan yang lebih rinci. Untuk memahami penutupan, penting untuk memahami bagaimana JavaScript membatasi variabel.

Lingkup

Dalam JavaScript, lingkup didefinisikan dengan fungsi. Setiap fungsi menentukan ruang lingkup baru.

Perhatikan contoh berikut;

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

memanggil f print

hello
hello
2
Am I Accessible?

Sekarang mari kita perhatikan kasus kita memiliki fungsi yang gdidefinisikan dalam fungsi lain f.

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

Kami akan memanggil fpara orang tua leksikal dari g. Seperti yang dijelaskan sebelumnya kita sekarang memiliki 2 cakupan; ruang lingkup fdan ruang lingkupg .

Tetapi satu ruang lingkup "di dalam" ruang lingkup yang lain, jadi apakah ruang lingkup fungsi anak bagian dari ruang lingkup fungsi orang tua? Apa yang terjadi dengan variabel yang dideklarasikan dalam ruang lingkup fungsi induk; Apakah saya dapat mengaksesnya dari ruang lingkup fungsi anak? Di situlah penutupan terjadi.

Penutupan

Dalam JavaScript fungsi gtidak hanya dapat mengakses variabel apa pun yang dideklarasikan dalam ruang lingkup gtetapi juga mengakses variabel yang dideklarasikan dalam ruang lingkup fungsi indukf .

Pertimbangkan untuk mengikuti;

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

memanggil f print

hello
undefined

Mari kita lihat garisnya console.log(foo);. Pada titik ini kami berada dalam cakupan gdan kami mencoba mengakses variabel fooyang dideklarasikan dalam lingkup f. Tetapi seperti yang dinyatakan sebelumnya kita dapat mengakses variabel apa pun yang dideklarasikan dalam fungsi induk leksikal yang merupakan kasus di sini; gadalah induk leksikal dari f. Karena helloitu dicetak.
Sekarang mari kita lihat garis console.log(bar);. Pada titik ini kami berada dalam cakupan fdan kami mencoba mengakses variabel baryang dideklarasikan dalam lingkup g. bartidak dideklarasikan dalam ruang lingkup saat ini dan fungsinya gbukan induk dari f, oleh karena itubar itu tidak terdefinisi

Sebenarnya kita juga dapat mengakses variabel yang dideklarasikan dalam ruang lingkup fungsi "grand parent" leksikal. Karena itu jika ada fungsi yang hdidefinisikan dalam fungsi tersebutg

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

maka hakan dapat mengakses semua variabel yang dideklarasikan dalam lingkup fungsi h, gdan f. Ini dilakukan dengan penutupan . Dalam penutupan JavaScript memungkinkan kita untuk mengakses variabel apa pun yang dideklarasikan dalam fungsi induk leksikal, dalam fungsi grand parent leksikal, dalam fungsi grand-grand parent leksikal, dll. Ini dapat dilihat sebagai rantai lingkup ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... sampai fungsi induk terakhir yang tidak memiliki induk leksikal.

Objek jendela

Sebenarnya rantai tidak berhenti pada fungsi induk terakhir. Ada satu ruang lingkup khusus lagi; yang lingkup global . Setiap variabel yang tidak dideklarasikan dalam suatu fungsi dianggap dideklarasikan dalam lingkup global. Ruang lingkup global memiliki dua spesialisasi;

  • setiap variabel yang dinyatakan dalam lingkup global dapat diakses di mana saja
  • variabel yang dideklarasikan dalam lingkup global sesuai dengan properti windowobjek.

Oleh karena itu ada dua cara untuk mendeklarasikan variabel foodalam lingkup global; baik dengan tidak mendeklarasikannya dalam suatu fungsi atau dengan mengatur properti fooobjek window.

Kedua upaya menggunakan penutupan

Sekarang setelah Anda membaca penjelasan yang lebih terperinci, kini mungkin tampak jelas bahwa kedua solusi menggunakan penutupan. Tapi yang pasti, mari kita buktikan.

Mari kita membuat Bahasa Pemrograman baru; JavaScript-Tanpa-Penutupan. Seperti namanya, JavaScript-No-Closure identik dengan JavaScript kecuali tidak mendukung Closures.

Dengan kata lain;

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

Baiklah, mari kita lihat apa yang terjadi dengan solusi pertama dengan 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)
  })();
}

karena itu ini akan mencetak undefined10 kali dalam JavaScript-No-Closure.

Oleh karena itu solusi pertama menggunakan penutupan.

Mari kita lihat solusi kedua;

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

karena itu ini akan mencetak undefined10 kali dalam JavaScript-No-Closure.

Kedua solusi menggunakan penutupan.

Sunting: Diasumsikan bahwa 3 snippet kode ini tidak didefinisikan dalam lingkup global. Kalau tidak, variabel foodan iakan mengikat ke windowobjek dan karena itu dapat diakses melalui windowobjek dalam JavaScript dan JavaScript-No-Closure

brillout.com
sumber
Kenapa harus itidak terdefinisi? Anda cukup merujuk ke lingkup orang tua, yang masih berlaku jika tidak ada penutupan.
Tema
untuk alasan yang sama seperti foo tidak terdefinisi dalam JavaScript-No-Closure. <code> i </code> tidak terdefinisi di JavaScript berkat fitur dalam JavaScript yang memungkinkan untuk mengakses variabel yang ditentukan dalam induk leksikal. Fitur ini disebut closure.
brillout
Anda tidak memahami perbedaan antara merujuk ke variabel yang sudah ditentukan dan variabel bebas . Sebagai penutup, kami mendefinisikan variabel bebas yang harus terikat dalam konteks luar. Dalam kode Anda, Anda hanya mengatur i2 untuk ipada saat Anda mendefinisikan fungsi Anda. Ini membuat iBUKAN variabel bebas. Namun, kami menganggap fungsi Anda sebagai penutupan, tetapi tanpa variabel bebas, itulah intinya.
Tema
2
@leem, saya setuju. Dan dibandingkan dengan jawaban yang diterima, ini tidak benar-benar menunjukkan apa yang sebenarnya terjadi. :)
Abel
3
Saya pikir ini adalah jawaban terbaik, menjelaskan penutupan secara umum dan sederhana kemudian masuk ke use case khusus. Terima kasih!
tim peterson
22

Saya tidak pernah senang dengan cara orang menjelaskan ini.

Kunci untuk memahami penutupan adalah untuk memahami akan seperti apa JS tanpa penutupan.

Tanpa penutupan, ini akan menimbulkan kesalahan

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

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

Setelah outerFunc kembali dalam versi JavaScript yang dinonaktifkan karena penutupan imajiner, referensi ke outerVar akan menjadi sampah yang dikumpulkan dan tidak meninggalkan apa pun di sana untuk fungsi internal sebagai referensi.

Penutupan pada dasarnya adalah aturan khusus yang mendukung dan memungkinkan vars tersebut ada saat fungsi dalam merujuk variabel fungsi luar. Dengan penutupan vars yang dirujuk dipertahankan bahkan setelah fungsi luar dilakukan atau 'ditutup' jika itu membantu Anda mengingat titik.

Bahkan dengan penutupan, siklus hidup vars lokal dalam suatu fungsi tanpa fungsi dalam yang mereferensikan penduduk lokalnya bekerja sama seperti dalam versi tanpa penutupan. Ketika fungsi selesai, penduduk setempat mengumpulkan sampah.

Setelah Anda memiliki referensi dalam fungsi internal ke var luar, namun itu seperti kusen pintu akan menghalangi pengumpulan sampah untuk vars yang direferensikan.

Cara yang mungkin lebih akurat untuk melihat penutupan, adalah bahwa fungsi bagian dalam pada dasarnya menggunakan lingkup dalam sebagai foudnation lingkupnya sendiri.

Tetapi konteks yang dirujuk sebenarnya, gigih, tidak seperti snapshot. Berulang kali menembakkan fungsi dalam yang dikembalikan yang terus bertambah dan mencatat var lokal fungsi luar akan terus mengingatkan nilai yang lebih tinggi.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
Erik Reppen
sumber
Anda benar tentang 'snapshot' (saya pikir, Anda merujuk pada jawaban saya) dengan itu. Saya sedang mencari kata yang akan menunjukkan perilaku. Dalam contoh Anda, ini dapat dilihat sebagai konstruksi penutupan 'hotlink'. Ketika menangkap penutupan sebagai parameter dalam fungsi-dalam, orang bisa menyatakan itu berperilaku sebagai 'snapshot'. Tapi saya setuju, kata-kata yang disalahgunakan hanya menambah kebingungan pada subjek. Jika Anda memiliki saran tentang itu, saya akan memperbarui jawaban saya.
Andries
Mungkin membantu dalam penjelasan jika Anda memberikan fungsi bagian dalam adalah fungsi bernama.
Phillip Senn
Tanpa penutupan, Anda akan mendapatkan kesalahan karena Anda mencoba menggunakan variabel yang tidak ada.
Juan Mendes
Hmm ... poin bagus. Apakah mereferensikan var yang tidak terdefinisi tidak pernah membuat kesalahan karena pada akhirnya akan terlihat sebagai properti pada objek global atau apakah saya bingung dengan penugasan ke vars yang tidak ditentukan?
Erik Reppen
17

Anda berdua menggunakan penutupan.

Saya akan menggunakan definisi Wikipedia di sini:

Dalam ilmu komputer, penutupan (juga penutupan leksikal atau penutupan fungsi) adalah fungsi atau referensi ke fungsi bersama-sama dengan lingkungan referensi — tabel yang menyimpan referensi ke masing-masing variabel non-lokal (juga disebut variabel bebas) dari fungsi tersebut . Penutupan - tidak seperti pointer fungsi polos - memungkinkan fungsi untuk mengakses variabel-variabel non-lokal bahkan ketika dipanggil di luar lingkup leksikal langsungnya.

Upaya teman Anda dengan jelas menggunakan variabel i, yang bukan lokal, dengan mengambil nilainya dan membuat salinan untuk disimpan ke lokali2 .

Upaya Anda sendiri diteruskan i(yang di situs panggilan berada dalam ruang lingkup) ke fungsi anonim sebagai argumen. Ini bukan penutupan sejauh ini, tapi kemudian fungsi itu mengembalikan fungsi lain yang referensi sama i2. Karena di dalam fungsi anonim dalam i2bukan lokal, ini membuat penutupan.

Jon
sumber
Ya, tapi saya pikir intinya adalah bagaimana dia melakukannya. Dia hanya salinan ike i2, kemudian mendefinisikan beberapa logika dan mengeksekusi fungsi ini. Jika saya tidak segera menjalankannya , tetapi menyimpannya dalam var, dan mengeksekusi setelah loop, itu akan mencetak 10, bukan? Jadi tidak menangkap saya.
Tema
6
@ Tema: Ini ditangkap dengan ibaik. Perilaku yang Anda gambarkan bukanlah hasil dari penutupan vs non-penutupan; itu adalah hasil dari variabel tertutup yang diubah sementara itu. Anda melakukan hal yang sama menggunakan sintaks yang berbeda dengan segera memanggil fungsi dan meneruskan isebagai argumen (yang menyalin nilainya saat ini di tempat). Jika Anda memasukkannya ke setTimeoutdalam yang lain setTimeout, hal yang sama akan terjadi.
Jon
13

Anda dan teman Anda berdua menggunakan penutupan:

Penutupan adalah jenis objek khusus yang menggabungkan dua hal: fungsi, dan lingkungan tempat fungsi itu dibuat. Lingkungan terdiri dari variabel lokal apa pun yang berada di ruang lingkup pada saat penutupan dibuat.

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

Dalam fungsi kode teman Anda function(){ console.log(i2); }didefinisikan dalam penutupan fungsi anonim function(){ var i2 = i; ...dan dapat membaca / menulis variabel lokali2 .

Dalam fungsi kode Anda function(){ console.log(i2); }didefinisikan di dalam penutupan fungsi function(i2){ return ...dan dapat membaca / menulis berharga lokali2 (dinyatakan dalam kasus ini sebagai parameter).

Dalam kedua kasus fungsi function(){ console.log(i2); }kemudian diteruskan kesetTimeout .

Setara lainnya (tetapi dengan pemanfaatan memori lebih sedikit) adalah:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}
Andrew D.
sumber
1
Saya tidak melihat mengapa solusi Anda vs solusi teman saya "lebih cepat dan dengan pemanfaatan memori lebih sedikit", bisakah Anda menjelaskan?
brillout
3
Dalam solusi Anda, Anda membuat 20 objek fungsi (2 objek pada setiap loop: 2x10 = 20). Hasil yang sama dalam solusi teman Anda. Dalam solusi "saya" hanya 11 objek fungsi yang dibuat: 1 sebelum untuk loop dan 10 "di dalam" - 1 + 1x10 = 11. Akibatnya - penggunaan memori lebih sedikit dan peningkatan kecepatan.
Andrew D.
1
Secara teori, itu benar. Dalam praktiknya, juga: Lihat patokan JSPerf ini: jsperf.com/closure-vs-name-function-in-a-loop/2
Rob W
10

Penutupan

Penutupan bukan fungsi, dan bukan ekspresi. Ini harus dilihat sebagai semacam 'snapshot' dari variabel yang digunakan di luar functionscope dan digunakan di dalam function. Secara tata bahasa, kita harus mengatakan: 'ambil penutup variabel'.

Sekali lagi, dengan kata lain: Penutupan adalah salinan dari konteks variabel yang relevan di mana fungsi bergantung.

Sekali lagi (naif): Penutupan memiliki akses ke variabel yang tidak dilewatkan sebagai parameter.

Ingatlah bahwa konsep-konsep fungsional ini sangat tergantung pada bahasa pemrograman / lingkungan yang Anda gunakan. Dalam JavaScript, penutupan tergantung pada pelingkupan leksikal (yang berlaku di sebagian besar bahasa-c).

Jadi, mengembalikan fungsi sebagian besar mengembalikan fungsi anonim / tanpa nama. Ketika variabel akses fungsi, tidak dilewatkan sebagai parameter, dan dalam lingkup (leksikal) nya, penutupan telah diambil.

Jadi, tentang contoh Anda:

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

Semua menggunakan penutupan. Jangan bingung titik eksekusi dengan penutupan. Jika 'snapshot' dari penutupan diambil pada saat yang salah, nilainya mungkin tidak terduga tetapi tentu saja penutupan diambil!

Andries
sumber
10

Mari kita lihat dua cara:

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

Deklarasikan dan segera jalankan fungsi anonim yang berjalan setTimeout()dalam konteksnya sendiri. Nilai saat ini dari idiawetkan dengan membuat salinan menjadi yang i2pertama; ini bekerja karena eksekusi segera.

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

Menyatakan konteks eksekusi untuk fungsi dalam di mana nilai saat iini dipertahankan ke dalam i2; pendekatan ini juga menggunakan eksekusi segera untuk mempertahankan nilai.

Penting

Harus disebutkan bahwa run semantik TIDAK sama antara kedua pendekatan; fungsi batin Anda diteruskan ke setTimeout()mana fungsi batinnya memanggil setTimeout()dirinya sendiri.

Membungkus kedua kode di dalam kode lain setTimeout()tidak membuktikan bahwa hanya pendekatan kedua yang menggunakan penutupan, tidak ada hal yang sama untuk memulai.

Kesimpulan

Kedua metode menggunakan penutupan, sehingga turun ke selera pribadi; pendekatan kedua lebih mudah untuk "bergerak" atau menyamaratakan.

Mendongkrak
sumber
Saya pikir perbedaannya adalah: Solusi-nya (1) ditangkap dengan referensi, milik saya (2) ditangkap oleh nilai. Dalam hal ini tidak ada bedanya, tetapi jika saya meletakkan eksekusi di setTimeout lain, kita akan melihat bahwa solusinya memiliki masalah yang kemudian menggunakan nilai akhir dari i, bukan saat ini, sementara tambang masih menggunakan nilai saat ini (karena ditangkap oleh nilai).
Tema
@leem Anda berdua tangkap dengan cara yang sama; meneruskan variabel melalui argumen fungsi atau penugasan adalah hal yang sama ... bisakah Anda menambahkan pertanyaan Anda bagaimana Anda akan menyelesaikan eksekusi di yang lain setTimeout()?
Ja͢ck
biarkan saya memeriksa ini ... Saya ingin menunjukkan bahwa objek fungsi dapat diedarkan dan variabel asli idapat diubah tanpa mempengaruhi apa fungsi yang harus dicetak, tidak tergantung di mana atau kapan kita menjalankannya.
Tema
Tunggu, Anda tidak melewatkan fungsi ke setTimeout (luar). Hapus itu (), sehingga melewati fungsi, dan Anda melihat 10 kali output 10.
Tema
@ tema Seperti yang disebutkan sebelumnya, ()inilah yang membuat kodenya berfungsi, seperti halnya Anda (i); Anda tidak hanya membungkus kodenya, Anda membuat perubahan terhadapnya .. karena itu Anda tidak dapat membuat perbandingan yang valid lagi.
Ja͢ck
8

Saya menulis ini beberapa waktu lalu untuk mengingatkan diri saya tentang apa penutupan itu dan bagaimana cara kerjanya di JS.

Penutupan adalah fungsi yang, ketika dipanggil, menggunakan ruang lingkup di mana ia dinyatakan, bukan ruang lingkup di mana ia dipanggil. Dalam javaScript, semua fungsi berperilaku seperti ini. Nilai variabel dalam ruang lingkup tetap ada selama ada fungsi yang masih menunjuk padanya. Pengecualian untuk aturan adalah 'ini', yang merujuk ke objek yang fungsinya ada di dalam ketika dipanggil.

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

Setelah memeriksa dengan cermat, sepertinya Anda berdua menggunakan penutupan.

Dalam kasus teman Anda, idiakses di dalam fungsi anonim 1 dan i2diakses di fungsi anonim 2 tempatconsole.log ada.

Dalam kasus Anda, Anda mengakses i2di dalam fungsi anonim di mana console.logada. Tambahkan debugger;pernyataan sebelum console.logdan di alat pengembang krom di bawah "Variabel lingkup" itu akan memberi tahu apa ruang lingkup variabel.

Ramesh
sumber
2
Bagian "Penutupan" di panel kanan digunakan karena tidak ada nama yang lebih spesifik. "Lokal" adalah indikasi yang lebih kuat daripada "Penutupan".
Rob W
4

Pertimbangkan yang berikut ini. Ini membuat dan menciptakan kembali fungsi fyang ditutup i, tetapi berbeda !:

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

sementara yang berikut ditutup pada "fungsi" itu sendiri "
(sendiri! potongan setelah ini menggunakan satu referensi f)

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

atau lebih eksplisit:

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

NB. definisi terakhir fadalah function(){ console.log(9) } sebelumnya 0 dicetak.

Peringatan! Konsep penutupan dapat menjadi gangguan paksaan dari esensi pemrograman dasar:

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

x-refs .:
Bagaimana cara kerja penutupan JavaScript?
Penjelasan Penutupan Javascript
Apakah Penutupan (JS) Membutuhkan Fungsi Di Dalam Fungsi
Bagaimana memahami penutupan dalam Javascript?
Javascript kebingungan variabel lokal dan global

ekim
sumber
cuplikan mencoba untuk pertama kalinya - tidak yakin bagaimana mengontrol - Run' only was desired - not sure how to remove the Salin`
ekim
-1

Saya ingin membagikan contoh dan penjelasan tentang penutupan. Saya membuat contoh python, dan dua angka untuk menunjukkan negara stack.

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

Output dari kode ini adalah sebagai berikut:

*****      hello      #####

      good bye!    ♥♥♥

Berikut adalah dua gambar untuk menunjukkan tumpukan dan penutupan yang melekat pada objek fungsi.

ketika fungsi dikembalikan dari pembuat

saat fungsi dipanggil nanti

Ketika fungsi dipanggil melalui parameter atau variabel nonlokal, kode membutuhkan binding variabel lokal seperti margin_top, padding serta a, b, n. Untuk memastikan kode fungsi berfungsi, bingkai tumpukan fungsi pembuat yang sudah lama hilang harus dapat diakses, yang didukung dalam penutupan yang dapat kita temukan bersama dengan objek pesan fungsi.

Eunjung Lee
sumber
Saya ingin menghapus jawaban ini. Saya menyadari bahwa pertanyaannya bukan tentang apa itu penutupan, jadi saya ingin memindahkannya ke pertanyaan lain.
Eunjung Lee
2
Saya yakin Anda memiliki kemampuan untuk menghapus konten Anda sendiri. Klik deletetautan di bawah jawabannya.
Rory McCrossan