Pintasan JavaScript untuk pemula

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(); // '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());  // 0 console.log(count());  // 1 console.log(count());  // 2 

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 Eksekusi

Ketika 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 panggilan

Ketika 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(); // '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());  // 0 console.log(count());  // 1 console.log(count());  // 2 

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.

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


All Articles