Closure

Fungsi yang mengingat lingkungannya dan gagasan besarnya.

Daftar isi

Sebuah fungsi yang dikembalikan dari fungsi lain membawa lingkungan variabel tempat ia ditulis. Pasangan fungsi beserta lingkungan variabel itulah yang disebut closure, yakni fungsi yang tetap dapat mengakses variabel lingkup asalnya setelah fungsi luarnya selesai dijalankan. Topik ini membuktikan perilaku itu lewat keluaran nyata, dimulai dari fungsi penyapa dan pembuat penghitung, lalu memisahkan closure dari variabel global yang sekilas mirip. Bagian akhir menghubungkan kedua contoh itu dengan gagasan besar closure, yaitu data yang tersimpan selama program berjalan (state) yang hanya terjangkau lewat fungsi tertentu.

Lingkungan variabel yang dibawa fungsi#

Setiap fungsi membawa catatan lingkungan variabel, yaitu kumpulan variabel yang terlihat oleh tubuhnya pada saat ia ditulis. Catatan itu dibentuk dari rantai scope tempat fungsi berada, bukan dari tempat fungsinya dipanggil. Selama fungsi masih terjangkau, misalnya karena disimpan di sebuah variabel, lingkungannya ikut tersimpan bersamanya. Contoh pertamanya adalah fungsi penyapa untuk buku tamu, dibuat oleh fungsi buatPenyapa yang menerima nama dan mengembalikan fungsi sapaan. Di sinilah closure berbeda dari pemanggilan fungsi biasa, fungsi luar sudah lama selesai, tetapi variabelnya tidak ikut terhapus.

js penyapa.js
// Fungsi yang kembali membawa lingkungan variabelnya.

function buatPenyapa(nama) {
  // parameter nama hidup di lingkup fungsi buatPenyapa
  return function () {
    return "Selamat datang, " + nama;
  };
}

const sapaBukuTamu = buatPenyapa("Dewi");

console.log(sapaBukuTamu());
text Keluaran
Selamat datang, Dewi

buatPenyapa dipanggil sekali dan selesai berjalan dalam sekejap, jauh sebelum baris console.log dieksekusi. Meski demikian, nama bernilai “Dewi” tetap terbaca oleh fungsi yang dikembalikannya. Karena nama tertulis di tubuh fungsi itu, lingkungan tempat nama berada ikut terbawa bersamanya. Parameter pun termasuk variabel yang terbawa, dan setiap pemanggilan dengan nama berbeda membentuk lingkungan yang berbeda pula. Tidak ada sintaks khusus yang perlu ditulis, membaca variabel dari lingkup luar sudah otomatis membentuk closure.

Dua penghitung, dua lingkungan terpisah#

Contoh berikut adalah pola pembuat penghitung yang menjadi peraga utama closure. Fungsi buatPenghitung mendeklarasikan langkah di lingkupnya, lalu mengembalikan fungsi naik yang hanya menaikkan langkah dan mengembalikan nilainya. Pertanyaannya muncul dengan sendirinya, apa yang terjadi bila pembuat ini dipanggil dua kali.

js buat-penghitung.js
// Setiap pemanggilan buatPenghitung membentuk lingkungannya sendiri.

function buatPenghitung() {
  let langkah = 0;

  return function naik() {
    langkah = langkah + 1;
    return langkah;
  };
}

const penghitungA = buatPenghitung();
const penghitungB = buatPenghitung();

console.log("A:", penghitungA());
console.log("A:", penghitungA());
console.log("B:", penghitungB());
console.log("A:", penghitungA());
console.log("B:", penghitungB());
text Keluaran
A: 1
A: 2
B: 1
A: 3
B: 2

Keluaran menunjukkan penghitungA berjalan dari 1 ke 2 lalu 3 tanpa terganggu, sedangkan penghitungB memulai dari 1 dan mencapai 2 dengan hitungannya sendiri. Artinya setiap pemanggilan buatPenghitung membentuk lingkungan baru berisi langkah miliknya, dan kedua closure itu tidak berbagi apa pun. Bila langkah dideklarasikan di luar fungsi sebagai satu variabel bersama, kedua penghitung akan menaikkan nilai yang sama dan keluarannya menjadi kacau.

Demo interaktif berikut mengulang pola ini di browser. Klik ketiga tombol penghitung secara bergantian, lalu perhatikan bahwa hitungan masing-masing tetap terpisah, termasuk ketika tombol terakhir mencoba membaca langkah langsung dari luar dan hanya menemukan undefined.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Demo closure penghitung</title>
  <style>
    body { font-family: "Segoe UI", Arial, sans-serif; margin: 14px; font-size: 14px; color: #1f2937; }
    button { padding: 6px 12px; margin-right: 8px; margin-bottom: 10px; cursor: pointer; }
    ol { font-size: 13px; line-height: 1.7; }
  </style>
</head>
<body>
  <p>Setiap tombol penghitung membawa closure-nya sendiri. Klik bergantian dan perhatikan hitungannya.</p>
  <button id="tombol-a" type="button">Penghitung A</button>
  <button id="tombol-b" type="button">Penghitung B</button>
  <button id="tombol-c" type="button">Penghitung C</button>
  <button id="tombol-baca" type="button">Baca penghitungA.langkah</button>
  <ol id="log"></ol>
  <script src="index.js"></script>

<script>
// Demo closure, tiga penghitung dengan lingkungan yang saling terpisah.

const log = document.getElementById("log");

function buatPenghitung() {
  let langkah = 0;
  return function naik() {
    langkah = langkah + 1;
    return langkah;
  };
}

function catat(pesan) {
  const baris = document.createElement("li");
  baris.textContent = pesan;
  log.appendChild(baris);
}

const penghitungA = buatPenghitung();
const penghitungB = buatPenghitung();
const penghitungC = buatPenghitung();

document.getElementById("tombol-a").addEventListener("click", () => {
  catat("Penghitung A: " + penghitungA());
});

document.getElementById("tombol-b").addEventListener("click", () => {
  catat("Penghitung B: " + penghitungB());
});

document.getElementById("tombol-c").addEventListener("click", () => {
  catat("Penghitung C: " + penghitungC());
});

document.getElementById("tombol-baca").addEventListener("click", () => {
  catat("penghitungA.langkah terbaca sebagai: " + penghitungA.langkah);
});

</script>
<script>
// Demo closure, tiga penghitung dengan lingkungan yang saling terpisah.

const log = document.getElementById("log");

function buatPenghitung() {
  let langkah = 0;
  return function naik() {
    langkah = langkah + 1;
    return langkah;
  };
}

function catat(pesan) {
  const baris = document.createElement("li");
  baris.textContent = pesan;
  log.appendChild(baris);
}

const penghitungA = buatPenghitung();
const penghitungB = buatPenghitung();
const penghitungC = buatPenghitung();

document.getElementById("tombol-a").addEventListener("click", () => {
  catat("Penghitung A: " + penghitungA());
});

document.getElementById("tombol-b").addEventListener("click", () => {
  catat("Penghitung B: " + penghitungB());
});

document.getElementById("tombol-c").addEventListener("click", () => {
  catat("Penghitung C: " + penghitungC());
});

document.getElementById("tombol-baca").addEventListener("click", () => {
  catat("penghitungA.langkah terbaca sebagai: " + penghitungA.langkah);
});

</script></body>
</html>

Diagram berikut merangkum mekanisme itu, fungsi yang dikembalikan selalu disertai kotak lingkungan variabelnya di mana pun fungsinya disimpan.

Dua pemanggilan buatPenghitung(), dua lingkungan terpisahpenghitungAfungsi naikmembawaLingkungan Alangkah = 3penghitungBfungsi naikmembawaLingkungan Blangkah = 2Kotak lingkungan tetap tersimpan bersama fungsinyasesudah buatPenghitung selesai dijalankan
Setiap fungsi naik membawa kotak lingkungan variabelnya sendiri setelah buatPenghitung selesai dijalankan

Tidak sama dengan variabel global#

Sekilas, variabel lingkungan closure yang bertahan lama mirip variabel global, tetapi keduanya berbeda jauh dalam hal akses. Variabel global dapat dibaca dan diganti dari baris mana pun, sehingga jejak perubahannya sulit diikuti pada program yang besar. Variabel lingkungan closure hanya terjangkau lewat fungsi yang membawanya, dan perubahannya hanya terjadi ketika fungsi itu dipanggil. Berkas berikut menaruh keduanya berdampingan agar bedanya terlihat langsung.

js beda-global.js
// Variabel global terjangkau dari mana pun, variabel closure tidak.

let totalGlobal = 0;

function buatPenghitungTerpisah() {
  let total = 0;

  return function naik() {
    total = total + 1;
    return total;
  };
}

const hitung = buatPenghitungTerpisah();
hitung();
hitung();

totalGlobal = 99;
console.log("totalGlobal diganti dari luar:", totalGlobal);

try {
  console.log(total);
} catch (e) {
  console.log("membaca total dari luar:", e.message);
}

console.log("hitung() berikutnya:", hitung());
text Keluaran
totalGlobal diganti dari luar: 99
membaca total dari luar: total is not defined
hitung() berikutnya: 3

totalGlobal diganti langsung dari luar tanpa keberatan apa pun, dan nilainya memang berubah menjadi 99. Sebaliknya, membaca total dari luar berakhir dengan ReferenceError karena nama itu tidak pernah terdaftar di scope global, ia hanya hidup di lingkungan yang dibawa hitung. Pemanggilan hitung() terakhir mengembalikan 3, bukti bahwa nilainya terus dipelihara di dalam lingkungannya tanpa pernah bisa disentuh langsung. Pada program sungguhan, perbedaan ini menentukan seberapa mudah perubahan sebuah nilai ditelusuri.

State yang hanya terjangkau lewat fungsi#

Gabungan dua sifat sebelumnya menghasilkan gagasan besar closure, yaitu data yang tersimpan selama program berjalan dan hanya terjangkau lewat fungsi tertentu. Variabelnya tidak bisa dibaca langsung, tidak bisa diganti langsung, dan hanya berubah melalui pemanggilan fungsi yang membawanya. Fungsi itu menjadi satu-satunya akses menuju datanya, sehingga aturan perubahan bisa ditaruh di dalam tubuhnya, misalnya batas nilai minimum dan maksimum pada sebuah saldo. Cara pandang ini mengubah peran fungsi, dari sekadar menghitung menjadi pengelola data miliknya sendiri.

Banyak fitur JavaScript yang terasa rumit berdiri di atas mekanisme yang sama. Callback masih mengingat konteksnya, modul menyembunyikan detailnya, dan event handler memakai data dari saat ia dipasang. Semuanya memanfaatkan lingkungan variabel yang terbawa bersama fungsinya. Membaca kode menjadi lebih ringan bila pertanyaan utamanya sederhana, yaitu lingkungan apa yang dibawa fungsi ini. Jawabannya ada pada tempat fungsi itu ditulis.

Ringkasan#

  • Closure adalah pasangan fungsi dengan lingkungan variabel tempat ia ditulis, yang tetap hidup setelah fungsi luarnya selesai dijalankan.
  • Setiap pemanggilan fungsi pembuat membentuk lingkungan baru, sehingga dua penghitung dari buatPenghitung yang sama berhitung terpisah.
  • Variabel lingkungan closure tidak terjangkau dari luar, berbeda dengan variabel global yang dapat diganti dari baris mana pun.
  • Gagasan besarnya adalah state yang hanya berubah lewat fungsi tertentu, dengan fungsi sebagai satu-satunya akses menuju datanya.

Referensi#