Pertanyaan Penutupan JavaScript di dalam loop - contoh praktis sederhana


var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

Ini menghasilkan ini:

Nilai saya: 3
  Nilai saya: 3
  Nilai saya: 3

Sedangkan saya ingin untuk output:

Nilai saya: 0
  Nilai saya: 1
  Nilai saya: 2


Masalah yang sama terjadi ketika penundaan dalam menjalankan fungsi disebabkan oleh menggunakan pendengar acara:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

... atau kode tidak sinkron, mis. menggunakan Janji:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for(var i = 0; i < 3; i++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

Apa solusi untuk masalah dasar ini?


2317
2018-04-15 06:06


asal


Jawaban:


Masalahnya adalah variabel itu i, dalam setiap fungsi anonim Anda, terikat ke variabel yang sama di luar fungsi.

Solusi klasik: Penutup

Apa yang ingin Anda lakukan adalah mengikat variabel dalam setiap fungsi ke nilai yang terpisah dan tidak berubah di luar fungsi:

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Karena tidak ada ruang lingkup blok di JavaScript - hanya lingkup fungsi - dengan membungkus pembuatan fungsi dalam fungsi baru, Anda memastikan bahwa nilai "i" tetap seperti yang Anda maksudkan.


Solusi 2015: forEach

Dengan ketersediaan yang relatif luas Array.prototype.forEach fungsi (pada tahun 2015), perlu dicatat bahwa dalam situasi-situasi yang melibatkan iterasi terutama atas serangkaian nilai, .forEach() menyediakan cara yang bersih dan alami untuk mendapatkan penutupan yang jelas untuk setiap iterasi. Yaitu, dengan asumsi Anda memiliki semacam array yang berisi nilai (referensi DOM, objek, apa pun), dan masalah muncul dari pengaturan callback khusus untuk setiap elemen, Anda dapat melakukan ini:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

Idenya adalah bahwa setiap permintaan fungsi callback digunakan dengan .forEach loop akan menjadi penutupnya sendiri. Parameter yang diteruskan ke handler tersebut adalah elemen array khusus untuk langkah iterasi tertentu. Jika digunakan dalam callback asynchronous, itu tidak akan bertabrakan dengan callback lain yang didirikan pada langkah-langkah iterasi lainnya.

Jika Anda bekerja di jQuery, $.each() fungsi memberi Anda kemampuan yang serupa.


Solusi ES6: let

ECMAScript 6 (ES6), versi terbaru dari JavaScript, sekarang mulai diimplementasikan di banyak browser dan sistem backend evergreen. Ada juga transpilers seperti Babel yang akan mengubah ES6 menjadi ES5 untuk memungkinkan penggunaan fitur-fitur baru pada sistem lama.

ES6 memperkenalkan yang baru let dan const kata kunci yang dilacak berbeda dari varvariabel berbasis. Misalnya, dalam satu lingkaran dengan a letIndeks berbasis, setiap iterasi melalui loop akan memiliki nilai baru i di mana setiap nilai dicakupkan di dalam loop, sehingga kode Anda akan berfungsi seperti yang Anda harapkan. Ada banyak sumber daya, tapi saya sarankan Pos pemblokiran 2ality sebagai sumber informasi yang bagus.

for (let i = 0; i < 3; i++) {
    funcs[i] = function() {
        console.log("My value: " + i);
    };
}

Hati-hati, meskipun, IE9-IE11 dan Edge sebelum Edge 14 mendukung let tetapi dapatkan kesalahan di atas (mereka tidak membuat yang baru i setiap kali, jadi semua fungsi di atas akan log 3 seperti yang akan mereka lakukan jika kami menggunakannya var). Ujung 14 akhirnya bisa melakukannya dengan benar.


1795
2018-04-15 06:18



Mencoba:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Edit (2014):

Secara pribadi saya pikir @ Aust jawaban terbaru tentang penggunaan .bind adalah cara terbaik untuk melakukan hal semacam ini sekarang. Ada juga lo-dash / underscore _.partial ketika Anda tidak perlu atau ingin mengacaukan bind's thisArg.


340
2018-04-15 06:10



Cara lain yang belum disebutkan adalah penggunaan Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

MEMPERBARUI

Seperti yang ditunjukkan oleh @squint dan @mekdev, Anda mendapatkan kinerja yang lebih baik dengan membuat fungsi di luar loop terlebih dahulu dan kemudian mengikat hasilnya di dalam loop.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


310
2017-10-11 16:41



Menggunakan sebuah Ekspresi Fungsi Segera Diaktifkan, cara paling sederhana dan paling mudah dibaca untuk menyertakan variabel indeks:

for (var i = 0; i < 3; i++) {

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

Ini mengirim iterator i ke dalam fungsi anonim yang kita definisikan sebagai index. Ini menciptakan penutupan, di mana variabel i disimpan untuk digunakan nanti dalam setiap fungsi asinkron dalam IIFE.


234
2017-10-11 18:23



Agak terlambat ke pesta, tetapi saya menjelajahi masalah ini hari ini dan memperhatikan bahwa banyak jawaban tidak sepenuhnya membahas bagaimana Javascript memperlakukan cakupan, yang pada intinya adalah apa inti dari hal ini.

Jadi seperti yang banyak disebutkan orang lain, masalahnya adalah bahwa fungsi batin mengacu sama i variabel. Jadi mengapa kita tidak membuat variabel lokal baru setiap iterasi, dan memiliki referensi fungsi dalam yang sebaliknya?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Sama seperti sebelumnya, di mana setiap fungsi bagian dalam mengeluarkan nilai terakhir yang ditetapkan i, sekarang setiap fungsi bagian dalam hanya menghasilkan nilai terakhir yang ditetapkan ilocal. Tetapi tidak setiap iterasi harus memiliki iterasi sendiri ilocal?

Ternyata itu masalahnya. Setiap iterasi berbagi lingkup yang sama, jadi setiap iterasi setelah yang pertama hanya timpa ilocal. Dari MDN:

Penting: JavaScript tidak memiliki cakupan blok. Variabel yang diperkenalkan dengan blok dicakupkan ke fungsi atau skrip yang mengandung, dan efek pengaturannya tetap ada di luar blok itu sendiri. Dengan kata lain, pernyataan blok tidak memperkenalkan ruang lingkup. Meskipun blok "mandiri" adalah sintaks yang valid, Anda tidak ingin menggunakan blok mandiri dalam JavaScript, karena mereka tidak melakukan apa yang Anda pikir mereka lakukan, jika Anda berpikir mereka melakukan sesuatu seperti blok tersebut di C atau Java.

Diulangi untuk penekanan:

JavaScript tidak memiliki cakupan blok. Variabel yang diperkenalkan dengan blok dicakupkan ke fungsi atau skrip yang mengandung

Kita bisa melihat ini dengan memeriksa ilocal sebelum kami mendeklarasikannya dalam setiap iterasi:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

Inilah tepatnya mengapa bug ini sangat rumit. Meskipun Anda mengulang variabel, Javascript tidak akan membuat kesalahan, dan JSLint bahkan tidak akan memberi peringatan. Ini juga mengapa cara terbaik untuk mengatasi ini adalah dengan memanfaatkan penutupan, yang pada dasarnya adalah ide yang di Javascript, fungsi-fungsi bagian dalam memiliki akses ke variabel luar karena cakupan dalam "melampirkan" lingkup luar.

Closures

Ini juga berarti bahwa fungsi-fungsi dalam "memegang" variabel luar dan membuatnya tetap hidup, bahkan jika fungsi luar kembali. Untuk memanfaatkan ini, kami membuat dan memanggil fungsi wrapper murni untuk membuat ruang lingkup baru, menyatakan ilocaldalam ruang lingkup baru, dan kembalikan fungsi batin yang digunakan ilocal (penjelasan lebih lanjut di bawah):

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Menciptakan fungsi batin di dalam fungsi pembungkus memberikan fungsi batin lingkungan pribadi yang hanya dapat diakses, "penutupan". Jadi, setiap kali kita memanggil fungsi pembungkus kita membuat fungsi batin baru dengan lingkungan yang terpisah itu sendiri, memastikan bahwa ilocal variabel tidak bertabrakan dan saling menimpa. Beberapa pengoptimalan kecil memberikan jawaban akhir yang diberikan banyak pengguna SO lainnya:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

Memperbarui

Dengan ES6 sekarang menjadi mainstream, kita sekarang dapat menggunakan yang baru let kata kunci untuk membuat variabel blok-cakupan:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

Lihatlah betapa mudahnya sekarang! Untuk informasi lebih lanjut, lihat jawaban ini, yang info saya didasarkan dari.


127
2018-04-10 09:57



Dengan ES6 sekarang didukung secara luas, jawaban terbaik untuk pertanyaan ini telah berubah. ES6 menyediakan let dan const kata kunci untuk keadaan yang tepat ini. Alih-alih bermain-main dengan penutupan, kita bisa menggunakannya let untuk mengatur variabel cakupan lingkaran seperti ini:

var funcs = [];
for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value: " + i); 
    };
}

val kemudian akan menunjuk ke suatu objek yang khusus untuk putaran tertentu dari loop, dan akan mengembalikan nilai yang benar tanpa tambahan penutupan tambahan. Ini jelas sangat menyederhanakan masalah ini.

const mirip dengan let dengan pembatasan tambahan bahwa nama variabel tidak dapat dipulihkan ke referensi baru setelah penugasan awal.

Dukungan browser sekarang di sini bagi mereka yang menargetkan versi terbaru dari browser. const/let saat ini didukung di Firefox, Safari, Edge, dan Chrome terbaru. Ini juga didukung di Node, dan Anda dapat menggunakannya di mana saja dengan memanfaatkan tool build seperti Babel. Anda dapat melihat contoh kerja di sini: http://jsfiddle.net/ben336/rbU4t/2/

Dokumen di sini:

Hati-hati, meskipun, IE9-IE11 dan Edge sebelum Edge 14 mendukung let tetapi dapatkan kesalahan di atas (mereka tidak membuat yang baru i setiap kali, jadi semua fungsi di atas akan log 3 seperti yang akan mereka lakukan jika kami menggunakannya var). Ujung 14 akhirnya bisa melakukannya dengan benar.


121
2018-05-21 03:04



Cara lain untuk mengatakannya adalah bahwa i dalam fungsi Anda terikat pada saat menjalankan fungsi, bukan saat membuat fungsi.

Saat Anda membuat penutupan, i adalah referensi ke variabel yang didefinisikan dalam lingkup luar, bukan salinannya seperti saat Anda membuat penutupan. Ini akan dievaluasi pada saat eksekusi.

Sebagian besar jawaban lainnya menyediakan cara untuk bekerja dengan menciptakan variabel lain yang tidak akan mengubah nilai untuk Anda.

Hanya berpikir saya akan menambahkan penjelasan untuk kejelasan. Untuk solusi, secara pribadi, saya akan pergi dengan Harto karena itu adalah cara paling jelas dalam melakukannya dari jawaban di sini. Setiap kode yang diposting akan bekerja, tapi saya akan memilih pabrik penutupan karena harus menulis setumpuk komentar untuk menjelaskan mengapa saya mendeklarasikan variabel baru (Freddy dan 1800) atau memiliki sintaks penutupan tersembunyi aneh (penipu).


77
2018-04-15 06:48



Yang perlu Anda pahami adalah ruang lingkup variabel dalam javascript didasarkan pada fungsi. Ini adalah perbedaan penting daripada mengatakan c # di mana Anda memiliki ruang lingkup blok, dan hanya menyalin variabel ke satu di dalam untuk akan bekerja.

Membungkusnya dalam fungsi yang mengevaluasi mengembalikan fungsi seperti jawaban juru mudi akan melakukan trik, karena variabel sekarang memiliki ruang lingkup fungsi.

Ada juga kata kunci yang dibiarkan, bukan var, yang memungkinkan menggunakan aturan lingkup blok. Dalam hal itu mendefinisikan variabel di dalam untuk akan melakukan trik. Meskipun begitu, kata kunci let bukanlah solusi praktis karena kompatibilitas.

var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value: " + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}

61
2018-04-15 06:25



Berikut variasi lain pada teknik ini, mirip dengan Bjorn (penawar), yang memungkinkan Anda menetapkan nilai variabel di dalam fungsi alih-alih meneruskannya sebagai parameter, yang mungkin lebih jelas kadang-kadang:

for (var i = 0; i < 3; i++) {
    funcs[i] = (function() {
        var index = i;
        return function() {
            console.log("My value: " + index);
        }
    })();
}

Perhatikan bahwa teknik apa pun yang Anda gunakan, index variabel menjadi semacam variabel statis, terikat pada salinan fungsi internal yang dikembalikan. Yaitu, perubahan nilainya dipertahankan antara panggilan. Ini bisa sangat berguna.


49
2017-08-07 08:45