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.
