Jika Anda pernah mengalami masalah JavaScript ini, Anda tidak sendirian - masalah ini membuat banyak orang tersandung dan pada awalnya bisa rumit, untuk memahami dengan tepat cara memperbaikinya. Mari pertimbangkan contoh ini
1var array = [ ... ]; // An array with some objects 2for( var i = 0; i < array.length; ++i ) 3{ 4 $.doSthWithCallbacks( function() { 5 array[i].something = 42; 6 }); 7}
javascript
Meskipun kode ini terlihat baik-baik saja, ini menunjukkan kesalahpahaman dari konsep JavaScript yang sangat mendasar. Sekarang, jika Anda berpengalaman dalam Javascript, kesalahan ini seharusnya cukup mudah dikenali. Tetapi bagi kebanyakan orang, ini bukan masalahnya - beberapa orang benar-benar dapat menghabiskan waktu berjam-jam untuk mencari tahu mengapa kode mereka tidak berfungsi
Penjelasan
Ingat tutorial JavaScript pertama yang Anda baca, yang mengatakan bahwa JavaScript tidak sinkron? . Hal ini biasanya terjadi saat menggunakan API internal yang bergantung pada peristiwa eksternal. Misalnya, memproses respons setelah permintaan HTTP selesai atau setelah beberapa pemrosesan lainnya selesai
Jadi yang terjadi kemudian, adalah doSthWithCallbacks (ekspresi umum untuk semua fungsi JavaScript yang menggunakan panggilan balik) menjadwalkan fungsi panggilan balik menjadi . Tapi for_ loop tidak hanya menjadwalkan satu callback. Ini menjadwalkan panggilan balik senilai array.length dan pasti tidak akan diselesaikan dalam waktu yang sama for loop iteration. Each of those callbacks will be executed at an unpredictable time later on, when multiple for iterasi telah dilakukan, nilai if 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}1 is different and multiple other callbacks have also been scheduled.
Biasanya, callback tidak dieksekusi sampai for loop selesai, pada titik mana . Jadi, setiap kali salah satu callback dieksekusi, itu akan mengubah nilai terakhir dari array alih-alih nilai iterasi loop is exactly equal to 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}4. So, every time any of the callbacks is executed it will be modifying the last value of the array instead of the value of the for yang dijadwalkan . Tentu saja, seperti yang saya katakan, tidak dapat diprediksi kapan callback akan dieksekusi dan bergantung pada banyak faktor yang digunakan oleh juru bahasa JavaScript, fungsi yang memanggil callback dan data inputnya. Contohnya adalah permintaan HTTP dengan callback sukses yang tidak akan dieksekusi sebelum server mengirimkan respons, yang bisa berupa interval waktu antara beberapa milidetik dan beberapa menit.
Bagaimana menyiasatinya
Saya akan memberi Anda dua solusi tentang cara mengatasi masalah tersebut. Keduanya sangat efisien dalam hal kinerja dan konsumsi memori. Metode pertama lebih mudah dipahami, tetapi memerlukan definisi fungsi di lingkup atas, yang membuat kode agak sulit dibaca karena Anda harus mencari fungsinya. Solusi kedua adalah favorit pribadi saya tetapi lebih sulit untuk dipahami, terutama ketika Anda melihat konstruksi seperti itu untuk pertama kalinya. Ada solusi lain, tetapi saat ini adalah yang tercepat dan didukung di semua browser utama dan juru bahasa JavaScript
Pada dasarnya kedua metode melakukan tugas yang sama tetapi dengan cara yang berbeda. Apa yang mereka lakukan adalah membuat fungsi panggilan balik terpisah dengan salinan mereka sendiri dari nilai 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}1 dalam cakupan yang hanya tersedia bagi mereka.
Penutupan dalam suatu fungsi
Metode ini relatif mudah dipahami dan itulah sebabnya saya tidak akan membahasnya secara mendetail. Ini tidak terjadi dengan penutupan inline, jadi saya akan membahasnya secara mendalam. Fungsi 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}7 mengembalikan fungsi yang memanggil callback aktual dengan salinan eksplisit 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}1 as an argument.
1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}_
javascript
Karena setiap fungsi menyatakan ruang lingkupnya sendiri, dan 1var array = [ ... ]; // An array with some objects 2 3function callbackClosure(i, callback) { 4 return function() { 5 return callback(i); 6 } 7} 8 9for( var i = 0; i < array.length; ++i ) 10{ 11 API.doSthWithCallbacks( callbackClosure( i, function(i) { 12 array[i].something = 42; 13 }) ); 14}1 memiliki tipe atom dasar ( 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function0< . ) it is not passed as a reference, but rather as a copy (unlike objects) which ensures that the actual callback will be executed against the correct value.
Penutupan sebaris
Ini membawa kita ke peretasan JavaScript favorit saya. Ini dilakukan dengan mendeklarasikan fungsi anonim yang disebut sendiri, yang umumnya terlihat seperti ini
1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function_
javascript
Perhatikan bahwa fungsi luar hanya digunakan untuk mengenkapsulasi fungsi dalam, dan membuat lingkup variabel terpisah untuk fungsi dalam. Selain itu, fungsi luar mengembalikan nilai bertipe 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function1 yang merupakan tipe yang tepat untuk callback. Jadi, menerapkan ini pada contoh sebelumnya kita sampai di sini.
1var array = [ ... ]; // An array with some objects 2for( var i = 0; i < array.length; ++i ) 3{ 4 API.doSthWithCallbacks( (function() { 5 var j = i; // j is a copy of i only available to the scope of the inner function 6 return function() { 7 array[j].something = 42; 8 } 9 })() ); 10}
javascript
Jika, misalnya, Anda harus melakukan beberapa pemrosesan asinkron, dan harus ada beberapa kode agregat yang hanya boleh dijalankan setelah semua callback selesai, yang perlu Anda lakukan adalah mengetahui berapa banyak callback yang Anda miliki . Jika 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function2 sama dengan 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function3 artinya Anda sedang memproses .
1var array = [ ... ]; // An array with some objects 2var count = 0, length = array.length; 3for( var i = 0; i < array.length; ++i ) 4{ 5 API.doSthWithCallbacks( (function() { 6 var j = i; // A copy of i only available to the scope of the inner function 7 return function() { 8 array[j].something = 42; 9 10 ++count; 11 if( count == length ) { 12 // Code executed only after all the processing tasks have been completed 13 } 14 } 15 })() ); 16}
javascript
Sekarang, pada titik ini mudah untuk menjadi bingung. Apakah ________9______4 operasi atomik? . Beberapa menganggap sesuatu seperti mutex atau semaphore. Tapi ini tidak benar.
Meskipun JavaScript asinkron, itu bukan multithreaded. Bahkan, meskipun tidak mungkin untuk memprediksi kapan panggilan balik akan dieksekusi, kondisi balapan dijamin tidak akan terjadi karena JavaScript hanya berjalan dalam satu utas. (Sebagai catatan tambahan, bukan berarti tidak ada cara untuk menjalankan banyak utas dalam JavaScript. Lihat API Pekerja Web untuk JavaScript jenis Web)
ES6
ECMAScript 6 memperkenalkan 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function5 kata kunci yang memungkinkan Anda mendeklarasikan variabel yang dicakup ke blok terlampir terdekat dan bukan seperti global 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function6 does. Thus the closure problem can be solved simply by replacing 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function6 dengan 1(function() { 2 // Something declared here will only be available to the function below. 3 // Code here is executed only once upon the creation of the inner function 4 return function(callbackArguments) { 5 // Actual callback here 6 }; 7})(); // The last brackets execute the outer function5 .
1var array = [ ... ]; // An array with some objects 2for( let i = 0; i < array.length; ++i ) 3{ 4 $.doSthWithCallbacks( function() { 5 array[i].something = 42; 6 }); 7}
js
Rapi, bukan?
Soal latihan
Saya telah menyiapkan Soal Latihan yang mendemonstrasikan masalahnya dan jika Anda mau, Anda dapat mencobanya. Saat mencobanya, hanya edit kode di dalam bagian yang ditentukan. Ada banyak cara untuk melakukannya, tetapi salah satu yang terbaik adalah menggunakan teknik kedua yang saya tunjukkan
Tentang Penulis
Itay Grudev adalah mahasiswa yang sedang mengejar gelar di bidang Ilmu Komputer dan Fisika di University of Aberdeen, Inggris