Pada chapter Scope kita menetapkan bahwa variabel di dalam function tidak terlihat dari luar, dan terasa wajar bila variabel itu ikut hilang setelah function selesai. Faktanya lebih menarik: bila sebuah function dalam dikirim keluar dari function luarnya, function dalam itu membawa serta lingkaran variabelnya — dan variabel tersebut tetap hidup selama function yang membawanya masih dirujuk. Fenomena ini disebut closure, dan merupakan salah satu konsep paling penting di seluruh JavaScript.
Cara paling cepat memahami closure adalah lewat masalah nyata: membuat penghitung yang aman. Chapter ini memulainya dari versi global yang mudah ditimpa siapa pun, lalu membangun versi closure, memperluasnya menjadi penyembunyian data, dan menutup dengan jebakan klasik yang wajib dikenali. Bila di akhir chapter variabel hitungan terasa seperti milik pribadi function tertentu — tidak terbaca, tidak tertimpa, tetapi terus bertambah — kita sudah memahami closure dengan benar.
Masalah: Hitungan Global yang Bisa Ditimpa #
Berkas berikut memulai dari versi paling naif, lalu memperbaikinya secara bertahap. Empat eksperimen dijalankan berurutan. Eksperimen pertama sengaja memperlihatkan kegagalan: hitungan berjalan, tetapi tidak ada yang melindunginya. Dua eksperimen berikut membangun versi closure dan memperluasnya menjadi penyembunyian data. Dua terakhir menunjukkan jebakan klasik yang wajib dikenali setiap programmer JavaScript:
// penghitung-closure.js
// Closure: function yang tetap mengingat lingkungan tempat ia dibuat.
// --- 1. masalah: hitungan dengan variabel global bisa diubah siapa saja ---
let jumlahKlik = 0; // global, terbuka bagi semua kode
function naikkanKlik() {
jumlahKlik = jumlahKlik + 1;
return jumlahKlik;
}
naikkanKlik();
naikkanKlik();
jumlahKlik = 99; // kode lain menimpa hitungan secara langsung
console.log("Klik setelah ditimpa:", naikkanKlik());
// --- 2. closure: hitungan tersembunyi di dalam function pembuatnya ---
function buatPenghitung() {
let hitungan = 0; // milik lingkaran buatPenghitung, bukan global
return function () {
hitungan = hitungan + 1;
return hitungan;
};
}
const hitungKunjungan = buatPenghitung();
const hitungPesan = buatPenghitung();
console.log("Kunjungan:", hitungKunjungan());
console.log("Kunjungan:", hitungKunjungan());
console.log("Pesan:", hitungPesan());
console.log("Kunjungan:", hitungKunjungan());
// --- 3. privasi data: kode luar tidak bisa menyentuh saldo langsung ---
function buatRekening(saldoAwal) {
let saldo = saldoAwal;
return {
setor: function (jumlah) {
saldo = saldo + jumlah;
},
lihatSaldo: function () {
return saldo;
},
};
}
const rekeningBudi = buatRekening(50000);
rekeningBudi.setor(20000);
console.log("Saldo Budi:", rekeningBudi.lihatSaldo());
// --- 4. jebakan klasik var di dalam loop: satu hitungan dipakai bersama ---
const tugas = [];
for (var i = 1; i <= 3; i++) {
tugas.push(function () {
console.log("Tugas nomor:", i);
});
}
tugas[0]();
tugas[1]();
tugas[2]();
// --- 5. solusi let: setiap putaran mendapat salinan i sendiri ---
const tugasRapi = [];
for (let j = 1; j <= 3; j++) {
tugasRapi.push(function () {
console.log("Tugas rapi nomor:", j);
});
}
tugasRapi[0]();
tugasRapi[1]();
tugasRapi[2]();Output yang Dihasilkan:
Klik setelah ditimpa: 100
Kunjungan: 1
Kunjungan: 2
Pesan: 1
Kunjungan: 3
Saldo Budi: 70000
Tugas nomor: 4
Tugas nomor: 4
Tugas nomor: 4
Tugas rapi nomor: 1
Tugas rapi nomor: 2
Tugas rapi nomor: 3Mari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 5–15 (eksperimen 1):
jumlahKlikglobal menaik dengan benar sampai baris 14 menimpanya dengan99— dari luar, tanpa melewati function apa pun. Pemanggilan berikutnya menghasilkan100, bukan3. Hitungan yang melompat dari 3 ke 100 ini bukan bug function-nya, melainkan kebocoran: state penting diletakkan di wilayah yang bisa diubah siapa pun. - Baris 18–24 (
buatPenghitung): solusinya terdiri dari dua lapis. Function luar menciptakanhitungan— lokal, bukan global — lalu mengembalikan function dalam yang membaca dan menaikkannya.hitunganhidup di lingkarbuatPenghitung; tidak ada jalur untuk menimpanya dari luar. - Baris 26–32 (dua penghitung): setiap pemanggilan
buatPenghitung()menciptakan lingkaranhitunganyang baru dan terpisah.hitungKunjungannaik1, 2, 3sementarahitungPesanpunya deretnya sendiri dari1. Tidak ada variabel global, dan tidak ada variabel yang bertabrakan — dua penghitung hidup berdampingan tanpa tahu satu sama lain. - Baris 35–49 (
buatRekening): versi yang lebih luas — function luar mengembalikan object berisi dua method yang sama-sama menutupsaldo. Kode luar hanya bisasetordanlihatSaldo; tidak ada cara langsung untuk membaca, apalagi menimpa,saldo. Uji kejujurannya dengan cara apa pun yang kita suka:rekeningBudi.saldotidak ada di sana, dan satu-satunya jalan masuk adalah method yang disediakan. Pola ini disebut data privasi lewat closure. - Baris 52–60 (jebakan
var): tiga function disimpan ke array selama loopvarberjalan.varhanya punya satu wadahiuntuk seluruh loop; saat ketiga function dipanggil, loop sudah selesai daniberisi4— karena itu ketiganya mencetak4. Ketiga function berbagi satu closure yang sama. - Baris 63–71 (solusi
let):letmenciptakan wadahjbaru di setiap putaran. Ketiga function kini masing-masing membawa closure denganjmiliknya:1,2,3. Satu kata kunci mengubah seluruh perilaku.
Mekanisme di Balik Closure #
Kita bisa merangkai aturannya dalam dua kalimat. Pertama, setiap kali function dalam dikirim keluar — lewat return, disimpan ke array, atau dikirim sebagai callback — ia menyimpan referensi ke lingkaran variabel tempat ia ditulis (lexical scope). Kedua, selama function itu masih dirujuk oleh variabel hidup, lingkarannya tidak dibersihkan: variabel seperti hitungan tetap ada di memori, dan hanya function-function yang menutupinya yang bisa mengaksesnya.
Inilah bedanya dengan chapter Scope: di sana lingkar function dianggap tertutup setelah eksekusi selesai. Closure adalah pengecualian yang tepat — lingkaran ikut keluar bersama function yang membawanya.
Factory: Satu Pola, Banyak Instansi #
Closure membuat mungkin pola yang disebut factory function: function pembuat yang mengembalikan function baru, lengkap dengan data pribadinya masing-masing. Contoh sederhananya adalah penyapa dengan gaya berbeda:
function buatPenyapa(sapaan) {
return function (nama) {
return sapaan + ", " + nama + "!";
};
}
const sapaFormal = buatPenyapa("Selamat pagi");
const sapaSantai = buatPenyapa("Hai");
console.log(sapaFormal("Budi"));
console.log(sapaSantai("Budi"));Output yang Dihasilkan:
Selamat pagi, Budi!
Hai, Budi!buatPenyapa dipanggil dua kali dan menghasilkan dua function. Keduanya ditulis dari template yang sama, tetapi masing-masing menutup nilai sapaan yang berbeda: milik sapaFormal menyimpan "Selamat pagi", milik sapaSantai menyimpan "Hai". sapaFormal("Budi") tidak menyentuh data milik sapaSantai — persis seperti hitungKunjungan dan hitungPesan di berkas contoh. Pola factory inilah yang nanti kita pakai berulang kali: membuat function yang dikonfigurasi sekali, lalu dipanggil berkali-kali.
Satu konsekuensi yang patut diingat sejak sekarang: variabel yang ditutup closure tetap menempati memori selama function penutupnya masih dirujuk. Untuk satu angka seperti hitungan biayanya tidak berarti; kebiasaan yang sehat adalah menutup hanya data yang memang dibutuhkan, bukan seluruh isi function luar yang besar.
Kapan Closure Berguna #
Tiga pola di berkas contoh adalah pola yang benar-benar dipakai di kode nyata: penghitung dan state privat tanpa variabel global, factory yang menghasilkan function berperilaku sama tetapi data berbeda, serta data yang hanya bisa disentuh lewat antarmuka yang disediakan. Callback dari chapter sebelumnya pun bekerja dengan cara yang sama: function anonim yang dikirim ke untukSetiap menutup lingkar variabel sekitarnya, sehingga ia bisa membaca data di luarnya saat dipanggil kembali. Nanti di section Browser Events, pola yang sama bertambah satu pemakai penting: penangan event yang menyimpan state halaman di lingkar variabelnya, bukan di variabel global.
Satu bentuk function yang belum kita sentuh sama sekali adalah function yang memanggil dirinya sendiri. Bentuk itu bernama recursion, dan merupakan topik penutup section ini.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
penghitung-closure.js
Unduh berkas// penghitung-closure.js
// Closure: function yang tetap mengingat lingkungan tempat ia dibuat.
// --- 1. masalah: hitungan dengan variabel global bisa diubah siapa saja ---
let jumlahKlik = 0; // global, terbuka bagi semua kode
function naikkanKlik() {
jumlahKlik = jumlahKlik + 1;
return jumlahKlik;
}
naikkanKlik();
naikkanKlik();
jumlahKlik = 99; // kode lain menimpa hitungan secara langsung
console.log("Klik setelah ditimpa:", naikkanKlik());
// --- 2. closure: hitungan tersembunyi di dalam function pembuatnya ---
function buatPenghitung() {
let hitungan = 0; // milik lingkaran buatPenghitung, bukan global
return function () {
hitungan = hitungan + 1;
return hitungan;
};
}
const hitungKunjungan = buatPenghitung();
const hitungPesan = buatPenghitung();
console.log("Kunjungan:", hitungKunjungan());
console.log("Kunjungan:", hitungKunjungan());
console.log("Pesan:", hitungPesan());
console.log("Kunjungan:", hitungKunjungan());
// --- 3. privasi data: kode luar tidak bisa menyentuh saldo langsung ---
function buatRekening(saldoAwal) {
let saldo = saldoAwal;
return {
setor: function (jumlah) {
saldo = saldo + jumlah;
},
lihatSaldo: function () {
return saldo;
},
};
}
const rekeningBudi = buatRekening(50000);
rekeningBudi.setor(20000);
console.log("Saldo Budi:", rekeningBudi.lihatSaldo());
// --- 4. jebakan klasik var di dalam loop: satu hitungan dipakai bersama ---
const tugas = [];
for (var i = 1; i <= 3; i++) {
tugas.push(function () {
console.log("Tugas nomor:", i);
});
}
tugas[0]();
tugas[1]();
tugas[2]();
// --- 5. solusi let: setiap putaran mendapat salinan i sendiri ---
const tugasRapi = [];
for (let j = 1; j <= 3; j++) {
tugasRapi.push(function () {
console.log("Tugas rapi nomor:", j);
});
}
tugasRapi[0]();
tugasRapi[1]();
tugasRapi[2]();