Menggunakan deklarasi biarkan variabel dan fitur dari penutupan yang dihasilkan dalam JavaScript

Saya terinspirasi untuk menulis catatan ini dengan membaca artikel di Habré "Var, let or const? Masalah lingkup variabel dan ES6 " dan komentarnya, serta bagian yang sesuai dari buku Zakas N." Memahami ECMASkrip 6 " . Berdasarkan apa yang saya baca, saya sampai pada kesimpulan bahwa tidak semuanya begitu sederhana dalam menilai penggunaan var atau membiarkan . Penulis dan komentator cenderung percaya bahwa dengan tidak adanya kebutuhan untuk mendukung versi browser yang lama, masuk akal untuk sepenuhnya meninggalkan penggunaan var , serta menggunakan beberapa konstruksi yang disederhanakan, bukan yang lama, secara default.

Sudah cukup dikatakan tentang ruang lingkup iklan ini, termasuk dalam materi di atas, jadi saya hanya ingin fokus pada beberapa poin yang tidak jelas.

Untuk memulainya, saya ingin mempertimbangkan ekspresi fungsi yang segera dipanggil (Immediately Invoked Function Expression, IIFE) dalam loop.
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 1 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

atau Anda dapat melakukannya tanpa mereka menggunakan biarkan :

 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); }); /*     0 newECMA6add.js:4:37 1 newECMA6add.js:4:37 2 newECMA6add.js:4:37 */ 

Zakas N. mengklaim bahwa kedua contoh serupa, memberikan hasil yang sama, juga bekerja persis sama:
"Loop ini bekerja persis seperti loop yang menggunakan var dan IIFE tetapi bisa dibilang lebih bersih"
yang, bagaimanapun, dia sendiri, sedikit lebih jauh, secara tidak langsung membantah.

Faktanya adalah bahwa setiap iterasi dari loop saat menggunakan let menciptakan variabel lokal i terpisah, sedangkan pengikatan dalam fungsi yang dikirim ke array juga pergi ke variabel yang terpisah dari setiap iterasi.

Dalam kasus khusus ini, hasilnya benar-benar tidak berbeda, tetapi bagaimana jika kita menyulitkan kodenya sedikit?
 let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

Di sini, menambahkan ++ i, hasil kami ternyata cukup dapat diprediksi, karena kami memanggil fungsi dengan nilai-nilai i yang relevan pada saat panggilan bahkan ketika loop itu sendiri berlalu, oleh karena itu operasi selanjutnya ++ i tidak mempengaruhi nilai yang diteruskan ke fungsi dalam array, karena sudah ditutup fungsi (i) dengan nilai spesifik i .

Sekarang bandingkan dengan versi bebas tanpa IIFE
 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); /*    1 newECMA6add.js:4:37 3 newECMA6add.js:4:37 */ 

Hasilnya, tampaknya, telah berubah, dan sifat dari perubahan ini adalah bahwa kami tidak segera memanggil fungsi dengan nilai, tetapi fungsi tersebut mengambil nilai-nilai yang tersedia dalam penutupan pada iterasi siklus tertentu.

Untuk lebih memahami esensi dari apa yang terjadi, pertimbangkan contoh dengan dua array. Dan sebagai permulaan, mari kita var, tanpa IIFE :
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    5 newECMA6add.js:6:37 6 newECMA6add.js:6:37 7 newECMA6add.js:5:37 8 newECMA6add.js:5:37 */ 

Semuanya jelas sejauh ini - tidak ada penutupan (meskipun kita dapat mengatakan bahwa itu adalah, tetapi untuk lingkup global, meskipun ini tidak sepenuhnya benar, karena akses ke saya pada dasarnya ada di mana-mana), yaitu, sama halnya, tetapi dengan area lokal rupanya, variabel saya akan memiliki entri serupa:
 let func1 = [], func2 = []; function test() { for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } } test(); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*     5 newECMA6add.js:7:41 6 newECMA6add.js:7:41 7 newECMA6add.js:6:41 8 newECMA6add.js:6:41 */ 

Dalam kedua contoh, berikut ini terjadi:

1. Pada awal iterasi terakhir dari siklus i == 2 , kemudian bertambah 1 (++ i) , dan pada akhirnya 1 lebih banyak ditambahkan dari i ++ , Akibatnya, pada akhir seluruh siklus i == 4 .

2. Fungsi-fungsi yang terletak di array func1 dan func2 disebut satu per satu , dan di masing-masing variabel i yang sama bertambah secara berurutan, yang dalam penutupan relatif terhadap cakupannya, yang terutama terlihat ketika kita berhadapan bukan dengan variabel global, tetapi dengan variabel lokal.

Tambahkan IIFE .
Opsi pertama:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 */ 
Opsi kedua:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(i); } }(++i)); func1.push(function(i) { return function() { console.log(i); } }(++i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:56 1 newECMA6add.js:5:56 */ 

Ketika menambahkan IIFE dalam kasus pertama, kita cukup memanggil nilai tetap fungsi i in (i) ( 0 dan 2 , selama pass pertama dan kedua dari siklus, masing-masing), dan menambahkannya dengan 1, masing-masing fungsi terpisah dari yang lain, karena di sini adalah penutupan untuk variabel umum tidak ada loop, karena fakta bahwa nilai saya ditransmisikan segera selama melewati loop. Dalam kasus kedua, juga tidak ada penutupan ke variabel loop, tetapi ada nilai yang ditransmisikan dengan kenaikan simultan, jadi pada akhir pass pertama i ==4 , dan loop tidak melangkah lebih jauh. Tapi, saya menarik perhatian pada fakta bahwa penutupan variabel dari fungsi eksternal di internal, untuk masing-masing fungsi secara terpisah, masih ada di kedua varian pertama dan kedua. Sebagai contoh:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 2 newECMA6add.js:6:56 4 newECMA6add.js:6:56 2 newECMA6add.js:5:56 4 newECMA6add.js:5:56 */ 
Catatan: bahkan jika Anda membingkai siklus dengan fungsi, penutupan umum secara alami tidak akan.

Sekarang pertimbangkan pernyataan let , tanpa IIFE.
 let func1 = [], func2 = []; for (let i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:41 4 newECMA6add.js:6:41 3 newECMA6add.js:5:41 5 newECMA6add.js:5:41 */ 

Dan di sini, kami sekali lagi membentuk hubungan pendek ke variabel loop, dan bukan satu, tetapi dua, dan tidak terpisah, tetapi umum, yang logis, mengingat prinsip let- in cycle yang terkenal.

Akibatnya, kami memiliki itu di penutupan pertama, sebelum memanggil fungsi di array, nilainya adalah i == 1 , dan di i kedua == 3 . Ini adalah nilai-nilai yang variabel saya terima sebelum i ++ dan iterasi loop, tetapi setelah semua instruksi di blok loop, dan, mereka ditutup untuk setiap iterasi tertentu.

Kemudian fungsi-fungsi yang terletak di array func1 dipanggil dan mereka menambah variabel yang sesuai di kedua penutupan dan sebagai hasilnya di i pertama == 2 , dan di kedua saya == 4 .

Panggilan selanjutnya ke func2 bertambah lebih jauh dan mendapatkan i == 3 dan 5, masing-masing.

Saya sengaja menempatkan func2 dan func1 di dalam blok sedemikian rupa sehingga independensi dari lokasi mereka lebih jelas terlihat, dan untuk menekankan perhatian pembaca pada fakta variabel closure to loop.

Sebagai kesimpulan, saya akan memberikan contoh sepele yang bertujuan memperkuat pemahaman tentang penutupan dan ruang lingkup let :
 let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i); /* 1 newECMA6add.js:5:34 ReferenceError: i is not definednewECMA6add.js:10:1 */ 

Apa yang kita miliki secara total


1. Memunculkan ekspresi fungsi yang dipanggil segera tidak setara dengan menggunakan variabel let iterable di fungsi dalam loop, dan, dalam beberapa kasus, mengarah ke hasil yang berbeda.

2. Karena kenyataan bahwa ketika menggunakan deklarasi let untuk iterator, variabel lokal yang terpisah dibuat di setiap iterasi, muncul pertanyaan tentang pembuangan data yang tidak perlu oleh pengumpul sampah. Pada titik ini, saya akui, saya awalnya ingin memusatkan perhatian, mencurigai bahwa membuat sejumlah besar variabel dalam jumlah besar, masing-masing, loop akan memperlambat kompiler, namun, ketika menyortir larik pengujian menggunakan hanya deklarasi variabel biarkan , itu menunjukkan keuntungan dalam waktu pelaksanaan hampir dua kali untuk array 100.000 sel:
Opsi dengan var:
 const start = Date.now(); var arr = [], func1 = [], func2 = []; for (var i = 0; i < 100000; i++) { arr.push(Math.random()); } for (var i = 0; i < 99999; i++) { var min, minind = i; for (var j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function(i) { return function() { return i; } }(arr[i])); } func1.push(function(i) { return function() { return i; } }(arr[99999])); for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 9.847 


Dan opsi dengan membiarkan:
 const start = Date.now(); let arr = [], func1 = [], func2 = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } for (let i = 0; i < 99999; i++) { let min, minind = i; for (let j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function() { return arr[i]; }); } func1.push(function() { return arr[99999]; }); for (let i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 5.3 


Selain itu, waktu pelaksanaan praktis independen dari ada / tidaknya instruksi:

dengan IIFE
 func1.push(function(i) { return function() { return i; } }(arr[i])); 

juga
tanpa IIFE
 func1.push(function() { return arr[i]; }); 

dan
panggilan fungsi
 for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } 


Catatan: Saya mengerti bahwa informasi tentang kecepatan bukanlah hal baru, tetapi untuk kelengkapan saya pikir kedua contoh ini layak untuk diberikan.

Dari semua ini kita dapat menyimpulkan bahwa penggunaan deklarasi let bukannya var , dalam aplikasi yang tidak memerlukan kompatibilitas dengan standar sebelumnya, lebih dari dibenarkan, terutama dalam kasus dengan loop. Tetapi, pada saat yang sama, ada baiknya mengingat fitur perilaku dalam situasi dengan penutupan dan, jika perlu, terus menggunakan ekspresi fungsi yang segera disebut.

Source: https://habr.com/ru/post/id462971/


All Articles