1. Lexical Scope (Cakupan Leksikal)
Di JavaScript, visibilitas suatu variabel ditentukan oleh lokasi fisik tempat variabel dan fungsi tersebut ditulis di dalam kode sumber—ini disebut Lexical Scoping (atau Static Scoping).
Bayangkan scope seperti kaca satu arah dari dalam ruangan ke luar:
- **Kode di cakupan dalam (inner scope)** selalu dapat melihat dan mengakses variabel yang berada di cakupan luarnya (outer/parent scope).
- **Kode di cakupan luar (outer scope) tidak pernah** dapat mengintip variabel yang dideklarasikan di dalam cakupan anak!
const namaAplikasi = "Kodeloka"; // 1. Global / Module Scope
function tampilkanKelas(namaKelas) {
// 2. Function Scope: bisa mengakses 'namaAplikasi' dan 'namaKelas'
const prefix = "[KELAS]";
if (namaKelas === "JavaScript") {
// 3. Block Scope: bisa mengakses 'prefix', 'namaKelas', dan 'namaAplikasi'
const tingkat = "Menengah";
console.log(`${prefix} ${namaAplikasi} - ${namaKelas} (${tingkat})`);
}
// ReferenceError! 'tingkat' hanya hidup di dalam blok kurung kurawal if { ... }
// console.log(tingkat);
}
Rantai Cakupan (Scope Chain)
Ketika Anda menggunakan sebuah variabel di dalam fungsi, mesin JavaScript mencarinya secara berjenjang ke atas: Cakupan Blok Saat Ini -> Cakupan Fungsi Induk -> Cakupan Fungsi Luar -> Cakupan Modul/Global. Begitu variabel ditemukan di salah satu lapis terdekat, pencarian berhenti. Jika sampai ke puncak global tetap tidak ditemukan, mesin melempar ReferenceError: x is not defined.
2. Hoisting dan Temporal Dead Zone (TDZ)
Istilah Hoisting merujuk pada cara mesin JavaScript mendaftarkan seluruh deklarasi variabel dan fungsi ke dalam memori cakupan sebelum mengeksekusi kode baris demi baris.
Namun, perilaku pendaftaran ini sangat berbeda antara function, var, dan let/const:
| Deklarasi | Apakah Terdaftar di Awal Scope? | Nilai Awal Sebelum Baris Deklarasinya Dieksekusi | Apa yang Terjadi Jika Diakses Sebelum Baris Deklarasinya? |
|---|---|---|---|
function nama() {} | Ya | Fungsi utuh siap pakai | Berhasil dijalankan dengan normal. |
var x = 10; | Ya (di tingkat fungsi) | Diinisialisasi sebagai undefined | Menghasilkan undefined secara diam-diam (sumber bug yang membingungkan!). |
let x = 10; / const y = 20; | Ya (di tingkat blok) | **Belum diinisialisasi (Uninitialized)** | Langsung melempar ReferenceError: Cannot access 'x' before initialization! |
Rentang baris sejak awal blok { hingga baris tempat let atau const dideklarasikan disebut Temporal Dead Zone (TDZ):
{
// --- Awal Temporal Dead Zone (TDZ) untuk variabel 'tarif' ---
// console.log(tarif); // ReferenceError! Aman dari bug pemakaian variabel sebelum waktunya.
const tarif = 15000; // --- Akhir TDZ: 'tarif' resmi diinisialisasi ---
console.log(tarif); // 15000
}
3. Apa Itu Closure dan Bagaimana Cara Kerjanya?
Closure adalah salah satu konsep paling kuat di JavaScript. Definisi resminya menurut MDN Web Docs:
Closure adalah gabungan dari sebuah fungsi beserta bundel referensi ke lingkungan leksikal (lexical environment) tempat fungsi tersebut diciptakan.
Secara sederhana: **ketika sebuah fungsi dalam (inner function) dikembalikan keluar dari fungsi induknya, fungsi dalam tersebut membawa "tas ransel" berisi variabel-variabel milik fungsi induk yang masih ia butuhkan**, sehingga variabel-variabel tersebut tidak dihapus oleh Garbage Collector meskipun fungsi induknya sudah selesai dijalankan!
buatPenghitung(awal = 0) selesai dijalankan, TETAPI: +-------------------------------------------------------+ | Fungsi yang dikembalikan (tambah) | | │ | | └── [Tas Ransel / Closure Environment] | | └── let nilai = 0 (Privat! Tidak bisa | | diakses langsung dari luar)| +-------------------------------------------------------+
Contoh Nyata 1: Mengemas State Privat (Data Encapsulation)
function buatDompet(saldoAwal = 0) {
// Variabel 'saldo' bersifat PRIVAT di dalam closure ini.
// Kode di luar tidak bisa mengubah 'saldo = 999999999' secara langsung!
let saldo = saldoAwal;
return {
lihatSaldo() {
return saldo;
},
setor(jumlah) {
if (jumlah <= 0) throw new Error("Jumlah setoran harus lebih dari 0.");
saldo += jumlah;
return saldo;
},
tarik(jumlah) {
if (jumlah > saldo) throw new Error("Saldo tidak mencukupi.");
saldo -= jumlah;
return saldo;
}
};
}
const dompetBudi = buatDompet(100_000);
dompetBudi.setor(50_000);
console.log(dompetBudi.lihatSaldo()); // 150000
console.log(dompetBudi.saldo); // undefined (Terlindungi!)
Contoh Nyata 2: Fungsi Pabrik (Function Factory) dan Pembatas Frekuensi (Debounce)
Di antarmuka web, saat pengguna mengetik di kotak pencarian, kita tidak ingin menembakkan permintaan API fetch() pada setiap ketukan huruf. Fungsi debounce menggunakan closure untuk menyimpan ID pengatur waktu (timerId) di antara ketukan huruf:
function buatDebounce(fungsiTarget, jedaMs = 300) {
let timerId = null; // Disimpan di dalam closure!
return function (...argumen) {
// Batalkan jadwal sebelumnya jika pengguna masih terus mengetik
clearTimeout(timerId);
// Jadwalkan ulang eksekusi setelah pengguna berhenti mengetik selama jedaMs
timerId = setTimeout(() => {
fungsiTarget(...argumen);
}, jedaMs);
};
}
4. Jebakan Klasik Closure di Dalam Perulangan (var vs let)
Mengapa dulu setTimeout di dalam perulangan for (var i = 0; ...) selalu mencetak angka terakhir yang sama?
- Karena
varmemiliki cakupan fungsi, sehingga semua callbacksetTimeoutberbagi satu variabeliyang sama di dalam closure mereka! - Sebaliknya,
letmemiliki cakupan blok, dan JavaScript menciptakan ikatan variabeliyang baru dan terpisah untuk setiap putaran iterasi:
// BENAR: Menggunakan 'let' menciptakan ikatan closure tersendiri di tiap iterasi (mencetak 1, 2, 3)
for (let i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(`Iterasi ke-${i}`);
}, i * 100);
}
Latihan Praktik: Membangun Fungsi Memoization (Cache Hasil Perhitungan) dengan Closure
Buatlah berkas closure-cache.js dan jalankan dengan Node.js:
- Buat fungsi
denganCache(fungsiHitung)yang menyimpan sebuahconst tembolok = new Map()di dalam closure-nya. - Fungsi yang dikembalikan menerima satu argumen
input. Jikatembolok.has(input)bernilaitrue, langsung kembalikan nilai daritembolok.get(input)tanpa menjalankan ulangfungsiHitung. - Jika belum ada, jalankan
const hasil = fungsiHitung(input), simpan ketembolok.set(input, hasil), lalu kembalikanhasil.