Penutupan adalah salah satu konsep dasar JavaScript, menyebabkan kesulitan bagi banyak pemula, yang harus diketahui dan dipahami oleh setiap programmer JS. Memiliki pemahaman yang baik tentang penutupan, Anda dapat menulis kode yang lebih baik, lebih efisien, dan lebih bersih. Dan ini, pada gilirannya, akan berkontribusi pada pertumbuhan profesional Anda.
Bahan, terjemahan yang kami terbitkan hari ini, dikhususkan untuk sebuah cerita tentang mekanisme internal penutupan dan bagaimana mereka bekerja dalam program JavaScript.
Apa itu penutupan?
Penutupan adalah fungsi yang memiliki akses ke ruang lingkup yang dibentuk oleh fungsi eksternal relatif terhadapnya, bahkan setelah fungsi eksternal ini menyelesaikan pekerjaannya. Ini berarti bahwa penutupan dapat menyimpan variabel yang dideklarasikan dalam fungsi eksternal dan argumen diteruskan ke sana. Sebelum kita melanjutkan ke penutupan, kita akan memahami konsep "lingkungan leksikal".
Apa itu lingkungan leksikal?
Istilah "lingkungan leksikal" atau "lingkungan statis" dalam JavaScript mengacu pada kemampuan untuk mengakses variabel, fungsi, dan objek berdasarkan lokasi fisik mereka dalam kode sumber. Pertimbangkan sebuah contoh:
let a = 'global';  function outer() {    let b = 'outer';    function inner() {      let c = 'inner'      console.log(c);   // 'inner'      console.log(b);   // 'outer'      console.log(a);   // 'global'    }    console.log(a);     // 'global'    console.log(b);     // 'outer'    inner();  } outer(); console.log(a);         // 'global' 
Di sini, fungsi 
inner() memiliki akses ke variabel yang dideklarasikan dalam cakupannya sendiri, dalam lingkup fungsi 
outer() , dan dalam lingkup global. Fungsi 
outer() memiliki akses ke variabel yang dideklarasikan dalam ruang lingkupnya sendiri dan dalam ruang lingkup global.
Rantai lingkup kode di atas akan terlihat seperti ini:
 Global { outer {   inner } } 
Perhatikan bahwa fungsi 
inner() dikelilingi oleh lingkungan leksikal dari fungsi 
outer() , yang pada gilirannya dikelilingi oleh cakupan global. Itulah sebabnya fungsi 
inner() dapat mengakses variabel yang dideklarasikan di fungsi 
outer() dan dalam lingkup global.
Contoh praktis dari penutupan
Pertimbangkan, sebelum membongkar seluk-beluk sirkuit internal, beberapa contoh praktis.
▍ Contoh No. 1
 function person() { let name = 'Peter'; return function displayName() {   console.log(name); }; } let peter = person(); peter();  
Di sini kita memanggil fungsi 
person() , yang mengembalikan fungsi internal 
displayName() , dan menyimpan fungsi ini dalam 
peter variabel. Ketika, setelah ini, kita memanggil fungsi 
peter() (variabel yang sesuai sebenarnya menyimpan referensi ke fungsi 
displayName() ), nama 
Peter ditampilkan di konsol.
Pada saat yang sama, tidak ada variabel 
displayName() dalam 
displayName() , sehingga kita dapat menyimpulkan bahwa fungsi ini entah bagaimana dapat mengakses variabel yang dinyatakan dalam fungsi eksternal untuk itu, 
person() , bahkan setelah itu bagaimana fungsi ini bekerja. Mungkin ini karena fungsi 
displayName() sebenarnya adalah penutup.
▍ Contoh No. 2
 function getCounter() { let counter = 0; return function() {   return counter++; } } let count = getCounter(); console.log(count());   
Di sini, seperti pada contoh sebelumnya, kami menyimpan tautan ke fungsi internal anonim yang dikembalikan oleh fungsi 
getCounter() dalam jumlah variabel. Karena fungsi 
count() adalah closure, ia dapat mengakses variabel 
counter dari fungsi 
getCount() bahkan setelah fungsi 
getCounter() telah menyelesaikan pekerjaannya.
Perhatikan bahwa nilai variabel 
counter tidak diatur ulang ke 0 setiap kali fungsi 
count() dipanggil. Mungkin tampaknya harus diatur ulang ke 0, seperti ketika memanggil fungsi biasa, tetapi ini tidak terjadi.
Ini berfungsi seperti itu karena setiap kali fungsi 
count() dipanggil, lingkup baru dibuat untuk itu, tetapi hanya ada satu lingkup untuk fungsi 
getCounter() . Karena variabel 
counter dideklarasikan dalam lingkup fungsi 
getCounter() , nilainya antara panggilan ke fungsi 
count() disimpan tanpa mengatur ulang ke 0.
Bagaimana cara kerja hubung singkat?
Sejauh ini, kami telah berbicara tentang apa penutupan itu, dan memeriksa contoh-contoh praktis. Sekarang mari kita bicara tentang mekanisme JavaScript internal yang membuatnya bekerja.
Untuk memahami penutupan, kita perlu berurusan dengan dua konsep JavaScript penting. Ini adalah Konteks Eksekusi dan Lingkungan Leksikal.
▍ Konteks eksekusi
Konteks eksekusi adalah lingkungan abstrak tempat kode JavaScript dihitung dan dieksekusi. Ketika kode global dieksekusi, ini terjadi di dalam konteks eksekusi global. Kode fungsi dieksekusi dalam konteks eksekusi fungsi.
Pada titik waktu tertentu, kode dapat dieksekusi hanya dalam satu konteks eksekusi (JavaScript adalah bahasa pemrograman single-threaded). Proses-proses ini dikelola menggunakan apa yang disebut Call Stack.
Tumpukan panggilan adalah struktur data yang disusun sesuai dengan prinsip LIFO (Last In, First Out - Last In, First Out). Elemen baru hanya dapat ditempatkan di bagian atas tumpukan, dan hanya elemen yang dapat dihapus darinya.
Konteks eksekusi saat ini akan selalu berada di atas tumpukan, dan ketika fungsi saat ini keluar, konteks eksekusi diambil dari tumpukan dan kontrol ditransfer ke konteks eksekusi, yang terletak di bawah konteks fungsi ini di tumpukan panggilan.
Pertimbangkan contoh berikut untuk lebih memahami apa konteks eksekusi dan tumpukan panggilan:
Contoh Konteks EksekusiKetika kode ini dieksekusi, mesin JavaScript membuat konteks eksekusi global untuk mengeksekusi kode global, dan ketika bertemu dengan fungsi 
first() , membuat konteks eksekusi baru untuk fungsi ini dan menempatkannya di atas tumpukan.
Tumpukan panggilan dari kode ini terlihat seperti ini:
Tumpukan panggilanKetika eksekusi fungsi 
first() selesai, konteks eksekusi diambil dari tumpukan panggilan dan kontrol ditransfer ke konteks eksekusi di bawahnya, yaitu ke konteks global. Setelah itu, kode yang tersisa di lingkup global akan dieksekusi.
EnvironmentLingkungan leksikal
Setiap kali mesin JS membuat konteks eksekusi untuk mengeksekusi fungsi atau kode global, itu juga menciptakan lingkungan leksikal baru untuk menyimpan variabel yang dideklarasikan dalam fungsi ini selama eksekusi.
Lingkungan leksikal adalah struktur data yang menyimpan informasi tentang korespondensi pengidentifikasi dan variabel. Di sini, "pengidentifikasi" adalah nama variabel atau fungsi, dan "variabel" adalah referensi ke objek (ini termasuk fungsi) atau nilai tipe primitif.
Lingkungan leksikal mengandung dua komponen:
- Catatan lingkungan adalah tempat di mana deklarasi variabel dan fungsi disimpan.
- Referensi ke lingkungan luar - tautan yang memungkinkan Anda untuk mengakses lingkungan leksikal (induk) eksternal. Ini adalah komponen paling penting yang perlu ditangani untuk memahami penutupan.
Secara konseptual, lingkungan leksikal terlihat seperti ini:
 lexicalEnvironment = { environmentRecord: {   <identifier> : <value>,   <identifier> : <value> } outer: < Reference to the parent lexical environment> } 
Lihatlah potongan kode berikut:
 let a = 'Hello World!'; function first() { let b = 25;  console.log('Inside first function'); } first(); console.log('Inside global execution context'); 
Ketika mesin JS menciptakan konteks eksekusi global untuk mengeksekusi kode global, itu juga menciptakan lingkungan leksikal baru untuk menyimpan variabel dan fungsi yang dideklarasikan dalam lingkup global. Akibatnya, lingkungan leksikal dari lingkup global akan terlihat seperti ini:
 globalLexicalEnvironment = { environmentRecord: {     a : 'Hello World!',     first : < reference to function object > } outer: null } 
Harap dicatat bahwa referensi ke lingkungan leksikal eksternal ( 
outer ) diatur ke 
null , karena ruang lingkup global tidak memiliki lingkungan leksikal eksternal.
Ketika mesin membuat konteks eksekusi untuk fungsi 
first() , itu juga menciptakan lingkungan leksikal untuk menyimpan variabel yang dideklarasikan dalam fungsi ini selama eksekusi. Akibatnya, lingkungan leksikal dari fungsi akan terlihat seperti ini:
 functionLexicalEnvironment = { environmentRecord: {     b : 25, } outer: <globalLexicalEnvironment> } 
Tautan ke lingkungan leksikal eksternal dari fungsi diatur ke 
<globalLexicalEnvironment> , karena dalam kode sumber kode fungsi berada dalam lingkup global.
Harap dicatat bahwa ketika fungsi selesai kerjanya, konteks eksekusi diambil dari tumpukan panggilan, tetapi lingkungan leksikalnya dapat dihapus dari memori, atau mungkin tetap ada. Itu tergantung pada apakah di lingkungan leksikal lain ada referensi ke lingkungan leksikal ini dalam bentuk tautan ke lingkungan leksikal eksternal.
Analisis rinci tentang contoh-contoh bekerja dengan penutupan
Sekarang setelah kami mempersenjatai diri dengan pengetahuan tentang konteks eksekusi dan lingkungan leksikal, kami akan kembali ke penutupan dan menganalisis fragmen kode yang sama lebih mendalam yang sudah kami periksa.
▍ Contoh No. 1
Lihatlah potongan kode ini:
 function person() { let name = 'Peter'; return function displayName() {   console.log(name); }; } let peter = person(); peter();  
Ketika fungsi 
person() dieksekusi, mesin JS menciptakan konteks eksekusi baru dan lingkungan leksikal baru untuk fungsi ini. Pekerjaan selesai, fungsi mengembalikan fungsi 
displayName() , referensi ke fungsi ini ditulis ke 
peter variabel.
Lingkungan leksikalnya akan terlihat seperti ini:
 personLexicalEnvironment = { environmentRecord: {   name : 'Peter',   displayName: < displayName function reference> } outer: <globalLexicalEnvironment> } 
Ketika fungsi 
person() keluar, konteks eksekusi muncul dari tumpukan. Tetapi lingkungan leksikalnya tetap ada dalam memori, karena ada tautan ke dalamnya di lingkungan leksikal dari fungsi internalnya 
displayName() . Akibatnya, variabel yang dideklarasikan dalam lingkungan leksikal ini tetap tersedia.
Ketika fungsi 
peter() dipanggil (variabel yang sesuai menyimpan referensi ke fungsi 
displayName() ), mesin JS menciptakan konteks eksekusi baru dan lingkungan leksikal baru untuk fungsi ini. Lingkungan leksikal ini akan terlihat seperti ini:
 displayNameLexicalEnvironment = { environmentRecord: {   } outer: <personLexicalEnvironment> } 
Tidak ada variabel dalam fungsi 
displayName() , jadi catatan lingkungannya akan kosong. Selama pelaksanaan fungsi ini, mesin JS akan mencoba menemukan variabel 
name di lingkungan leksikal dari fungsi.
Karena pencarian tidak dapat ditemukan di lingkungan leksikal dari fungsi 
displayName() , pencarian akan berlanjut di lingkungan leksikal eksternal, yaitu di lingkungan leksikal fungsi 
person() , yang masih dalam memori. Di sana, mesin menemukan variabel yang diinginkan dan menampilkan nilainya di konsol.
▍ Contoh No. 2
 function getCounter() { let counter = 0; return function() {   return counter++; } } let count = getCounter(); console.log(count());   
Lingkungan leksikal dari fungsi 
getCounter() akan terlihat seperti ini:
 getCounterLexicalEnvironment = { environmentRecord: {   counter: 0,   <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> } 
Fungsi ini mengembalikan fungsi anonim yang ditugaskan ke variabel 
count .
Ketika fungsi 
count() dieksekusi, lingkungan leksikalnya terlihat seperti ini:
 countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> } 
Saat melakukan fungsi ini, sistem akan mencari variabel 
counter di lingkungan leksikalnya. Dalam kasus ini, sekali lagi, catatan lingkungan fungsi kosong, sehingga pencarian variabel berlanjut di lingkungan leksikal eksternal dari fungsi.
Mesin menemukan variabel, menampilkannya di konsol, dan menambah variabel 
counter , yang disimpan dalam lingkungan leksikal dari fungsi 
getCounter() .
Akibatnya, lingkungan leksikal dari fungsi 
getCounter() setelah panggilan pertama ke fungsi 
count() akan terlihat seperti ini:
 getCounterLexicalEnvironment = { environmentRecord: {   counter: 1,   <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> } 
Setiap kali fungsi 
count() dipanggil, mesin JavaScript membuat lingkungan leksikal baru untuk fungsi ini dan menambah variabel 
counter , yang mengarah pada perubahan lingkungan leksikal dari fungsi 
getCounter() .
Ringkasan
Pada artikel ini, kami berbicara tentang apa penutupan itu dan menyortir mekanisme JavaScript yang mendasarinya. Penutupan adalah salah satu konsep dasar JavaScript yang paling penting, dan setiap pengembang JS harus memahaminya. Memahami penutupan adalah salah satu langkah untuk menulis aplikasi yang efektif dan berkualitas tinggi.
Pembaca yang budiman! Jika Anda memiliki pengalaman dalam pengembangan JS, silakan bagikan contoh praktis menggunakan penutupan dengan pemula.
