Python

πŸ”’ Closures & Scope di JavaScript

Pahami konsep paling fundamental di JavaScript β€” global scope, function scope, block scope, closure, dan lexical environment dengan penjelasan mendalam dan contoh kode praktis

1. Apa Itu Scope?

Scope (lingkup) adalah konteks di mana variabel dan fungsi "hidup" dan bisa diakses. Setiap kali Anda mendeklarasikan variabel, variabel tersebut hanya bisa diakses dari area tertentu dalam kode β€” area inilah yang disebut scope. Pemahaman scope adalah kunci untuk menghindari bug, menulis kode yang bersih, dan memahami konsep lanjutan seperti closure.

Di JavaScript, ada tiga jenis scope utama: Global Scope, Function Scope, dan Block Scope. Masing-masing memiliki aturan tersendiri tentang kapan dan di mana sebuah variabel bisa diakses. Selain itu, JavaScript memiliki konsep Lexical Environment yang menentukan bagaimana scope nested saling berhubungan melalui scope chain.

Tiga Jenis Scope

Scope Dibuat Oleh Akses Kata Kunci
Global ScopeKode di luar fungsi/blokDiakses dari mana sajavar, let, const
Function ScopeFungsiHanya di dalam fungsivar, let, const
Block Scope{} (if, for, dll)Hanya di dalam bloklet, const saja
Diagram: Scope Layers
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  GLOBAL SCOPE                     β”‚
β”‚   window, document, var globalVar = "hello"       β”‚
β”‚                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              FUNCTION SCOPE                  β”‚  β”‚
β”‚  β”‚   function myFunc() {                        β”‚  β”‚
β”‚  β”‚     var a = 1;  let b = 2;                   β”‚  β”‚
β”‚  β”‚                                              β”‚  β”‚
β”‚  β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚  β”‚
β”‚  β”‚    β”‚         BLOCK SCOPE               β”‚     β”‚  β”‚
β”‚  β”‚    β”‚   if (true) {                     β”‚     β”‚  β”‚
β”‚  β”‚    β”‚     let c = 3; const d = 4;       β”‚     β”‚  β”‚
β”‚  β”‚    β”‚     // c, d hanya di sini         β”‚     β”‚  β”‚
β”‚  β”‚    β”‚   }                               β”‚     β”‚  β”‚
β”‚  β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚  β”‚
β”‚  β”‚                                              β”‚  β”‚
β”‚  β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚  β”‚
β”‚  β”‚    β”‚         BLOCK SCOPE               β”‚     β”‚  β”‚
β”‚  β”‚    β”‚   for (let i = 0; i < 10; i++) {  β”‚     β”‚  β”‚
β”‚  β”‚    β”‚     // i hanya di sini            β”‚     β”‚  β”‚
β”‚  β”‚    β”‚   }                               β”‚     β”‚  β”‚
β”‚  β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚  β”‚
β”‚  β”‚   }                                         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Global Scope

Global scope adalah scope terluar β€” semua variabel yang dideklarasi di luar fungsi atau blok kode manapun berada di global scope. Variabel global bisa diakses dari mana saja dalam program, termasuk dari dalam fungsi dan blok. Di browser, global scope diwakili oleh object window; di Node.js oleh global atau globalThis.

JavaScript β€” Global Scope
// Variabel global β€” bisa diakses dari mana saja
var namaGlobal = "BeebaneLabs";
let versiGlobal = "1.0.0";
const API_URL = "https://api.example.com";

function tampilkanNama() {
  // Bisa mengakses variabel global dari dalam fungsi
  console.log(namaGlobal);   // "BeebaneLabs"
  console.log(API_URL);      // "https://api.example.com"
}
tampilkanNama();

// Di browser: variabel global menjadi properti window
var city = "Jakarta";
console.log(window.city);    // "Jakarta"

// TAPI: let dan const TIDAK menjadi properti window
let name = "Budi";
console.log(window.name);    // "" (bukan "Budi!")

// ⚠️ BAHAYA: Terlalu banyak variabel global
// 1. Namespace pollution β€” mudah bentrok nama
// 2. Siapa saja bisa mengubah β€” sulit dilacak
// 3. Memory β€” tidak pernah di-garbage-collect

// βœ… BEST PRACTICE: Kurangi variabel global
// Gunakan module pattern atau ES modules
const App = {
  nama: "BeebaneLabs",
  versi: "1.0.0",
  init() {
    console.log(`${this.nama} v${this.versi}`);
  }
};
App.init();  // "BeebaneLabs v1.0.0"
πŸ’‘ Tips

Hindari penggunaan variabel global sebisa mungkin. Jika Anda perlu menyimpan state yang bisa diakses dari banyak tempat, gunakan module pattern, ES modules (import/export), atau state management library. Variabel global adalah salah satu penyebab utama bug yang sulit dilacak di aplikasi besar.

3. Function Scope

Setiap kali Anda membuat fungsi, JavaScript membuat function scope baru. Variabel yang dideklarasi di dalam fungsi (dengan var, let, atau const) hanya bisa diakses dari dalam fungsi tersebut. Ini berlaku untuk parameter fungsi juga β€” parameter berperan sebagai variabel lokal di dalam function scope.

JavaScript β€” Function Scope
// Setiap fungsi menciptakan scope baru
function sapaOrang(nama) {
  // nama adalah parameter β€” berperan sebagai variabel lokal
  let pesan = `Halo, ${nama}!`;  // lokal di fungsi ini
  const waktu = new Date().toLocaleTimeString("id-ID");
  console.log(`${pesan} Waktu: ${waktu}`);

  // Bisa membuat fungsi di dalam fungsi (nested)
  function formatPesan() {
    return `[${waktu}] ${pesan}`;
  }
  console.log(formatPesan());
}

sapaOrang("Budi");
// console.log(nama);    // ❌ ReferenceError: nama is not defined
// console.log(pesan);   // ❌ ReferenceError: pesan is not defined
// console.log(waktu);   // ❌ ReferenceError: waktu is not defined

// Variabel dalam fungsi bersifat LOKAL β€” tidak mempengaruhi global
function testScope() {
  var x = 10;       // Lokal di fungsi ini
  let y = 20;       // Lokal di fungsi ini
  console.log(x, y); // 10, 20
}

var x = 100;        // Variabel global (beda dengan x di dalam fungsi)
testScope();
console.log(x);      // 100 (bukan 10!)

// ⚠️ var di function scope: tidak terpengaruh blok
function varBehavior() {
  if (true) {
    var a = 1;  // Tetap di function scope, BUKAN block scope
    let b = 2;  // Di block scope
    const c = 3; // Di block scope
  }
  console.log(a);  // 1 ← var "bocor" keluar blok!
  // console.log(b);  // ❌ ReferenceError
  // console.log(c);  // ❌ ReferenceError
}
varBehavior();

// IIFE β€” Immediately Invoked Function Expression
// Membuat function scope tanpa memberi nama fungsi
(function() {
  var rahasia = "hanya bisa diakses di dalam sini";
  console.log(rahasia);  // βœ…
})();
// console.log(rahasia);  // ❌ ReferenceError

// IIFE dengan parameter
(function(nama) {
  console.log(`Hello, ${nama}!`);
})("BeebaneLabs");
// Output: Hello, BeebaneLabs!

4. Block Scope

Block scope adalah scope yang dibatasi oleh kurung kurawal {}. Di ES6+, let dan const menghormati block scope β€” variabel yang dideklarasi dengan let/const di dalam blok hanya bisa diakses di dalam blok tersebut. Ini berbeda dengan var yang hanya mengenal function scope.

JavaScript β€” Block Scope
// Block scope dibuat oleh {}
// if, for, while, switch, atau blok kosong

// if statement
if (true) {
  let status = "aktif";
  const kode = 123;
  var globalBocor = "aku bocor!";
  console.log(status);  // βœ… "aktif"
}
// console.log(status);      // ❌ ReferenceError
// console.log(kode);        // ❌ ReferenceError
console.log(globalBocor);    // βœ… "aku bocor!" β€” var bocor!

// for loop β€” perbedaan var vs let yang KRUSIAL
console.log("=== for loop dengan var ===");
for (var i = 0; i < 3; i++) {
  // Semua iterasi berbagi var i yang SAMA
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3  ← BUG! (semua cetak 3 karena i sudah selesai loop)

console.log("=== for loop dengan let ===");
for (let j = 0; j < 3; j++) {
  // Setiap iterasi punya let j yang BERBEDA
  setTimeout(() => console.log(j), 100);
}
// Output: 0, 1, 2  ← BENAR! (setiap iterasi punya j sendiri)

// switch statement
switch (new Date().getDay()) {
  case 0: {
    let hari = "Minggu";
    console.log(hari);
    break;
  }
  case 1: {
    let hari = "Senin";
    console.log(hari);
    break;
  }
}
// Variabel hari di dalam case tidak bentrok karena block scope

// Nested blocks
{
  let level = 1;
  console.log(level);  // 1

  {
    let level = 2;
    console.log(level);  // 2

    {
      let level = 3;
      console.log(level);  // 3
    }

    console.log(level);  // 2 (bukan 3!)
  }

  console.log(level);  // 1 (bukan 2!)
}
πŸ’‘ Tips

Salah satu bug paling terkenal di JavaScript lama adalah loop for dengan var di dalam setTimeout/click handler. Semua callback akan merujuk ke nilai i yang sama (nilai akhir). Dengan let, setiap iterasi mendapat binding i sendiri. Ini adalah alasan kuat untuk selalu menggunakan let dan const, bukan var.

5. Lexical Environment

Lexical Environment adalah struktur data internal yang digunakan JavaScript untuk melacak variabel. Setiap kali fungsi dieksekusi atau blok kode dijalankan, JavaScript membuat Lexical Environment baru yang berisi: (1) environment record (daftar variabel lokal), dan (2) referensi ke outer environment (parent scope). Konsep ini adalah dasar dari bagaimana scope chain dan closure bekerja.

JavaScript β€” Lexical Environment
// Setiap execution context punya Lexical Environment
// Terdiri dari:
// 1. Environment Record β€” semua variabel lokal
// 2. Outer Reference β€” referensi ke parent Lexical Env

// Contoh sederhana
let x = 10;  // Global Lexical Environment: { x: 10, outer: null }

function outer() {
  let y = 20;  // outer Lexical Env: { y: 20, outer: global }
  
  function inner() {
    let z = 30;  // inner Lexical Env: { z: 30, outer: outer }
    
    // JavaScript mencari variabel:
    // 1. Di environment record lokal β†’ tidak ada x
    // 2. Naik ke outer (outer function) β†’ tidak ada x
    // 3. Naik ke global β†’ x = 10 βœ…
    console.log(x + y + z);  // 10 + 20 + 30 = 60
  }
  
  inner();
}
outer();

// "Lexical" = ditentukan oleh LOKASI kode ditulis (di source code)
// Bukan lokasi kode dipanggil

function buatCounter() {
  let count = 0;  // Lexical environment milik buatCounter
  
  return function() {
    count++;  // Masih bisa akses 'count' dari parent
    return count;
  };
}

const counter = buatCounter();
console.log(counter());  // 1
console.log(counter());  // 2
console.log(counter());  // 3
// 'count' tetap hidup karena Lexical Environment dipertahankan

// Visualisasi Lexical Environment Chain:
//
// inner() LE:
//   { z: 30 }  ──outer──▢  outer() LE:
//                              { y: 20 }  ──outer──▢  Global LE:
//                                                          { x: 10, outer: null }

Hoisting dan Lexical Environment

JavaScript β€” Hoisting
// HOISTING = variabel & fungsi "dinaikkan" ke atas scope

// var: di-hoist sebagai undefined
console.log(a);  // undefined (bukan error!)
var a = 10;
console.log(a);  // 10

// let/const: di-hoist tapi TIDAK bisa diakses sebelum deklarasi
// (Temporal Dead Zone β€” TDZ)
// console.log(b);  // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 20;

// Function declaration: di-hoist SEPENUHNYA
salam();  // βœ… Bisa dipanggil sebelum deklarasi!
function salam() {
  console.log("Halo!");
}

// Function expression: mengikuti aturan var/let/const
// greet();  // ❌ TypeError: greet is not a function
var greet = function() {
  console.log("Hello!");
};
greet();  // βœ… Setelah deklarasi

// Arrow function
// hello();  // ❌ TypeError
const hello = () => console.log("Hi!");
hello();  // βœ…

// ⚠️ TDZ (Temporal Dead Zone)
function testTDZ() {
  // 'x' ada di TDZ dari awal fungsi sampai deklarasi
  // console.log(x);  // ❌ ReferenceError
  
  let x = 10;  // TDZ berakhir di sini
  console.log(x);  // βœ… 10
}
testTDZ();

6. Scope Chain

Scope chain adalah rantai referensi dari satu Lexical Environment ke parent-nya. Ketika JavaScript mencari sebuah variabel, ia pertama-tama mencari di scope lokal. Jika tidak ditemukan, ia naik ke parent scope, lalu ke grandparent, dan seterusnya hingga mencapai global scope. Jika tetap tidak ditemukan, JavaScript melempar ReferenceError.

JavaScript β€” Scope Chain
// Pencarian variabel mengikuti scope chain
let global = "global";

function level1() {
  let l1 = "level 1";

  function level2() {
    let l2 = "level 2";

    function level3() {
      let l3 = "level 3";

      // Pencarian: l3 β†’ level2 β†’ level1 β†’ global β†’ ReferenceError
      console.log(l3);     // "level 3" (ditemukan di level3)
      console.log(l2);     // "level 2" (naik ke level2)
      console.log(l1);     // "level 1" (naik ke level1)
      console.log(global); // "global" (naik ke global)
    }

    level3();
  }

  level2();
}

level1();

// Scope chain hanya SATU ARAH: dari dalam ke luar
// Scope luar TIDAK bisa mengakses scope dalam
function parent() {
  let rahasia = "rahasia parent";

  function child() {
    let dataAnak = "data anak";
    console.log(rahasia);  // βœ… Bisa akses parent
  }

  child();
  // console.log(dataAnak);  // ❌ Tidak bisa akses child
}

// Contoh: variable shadowing
let nama = "Global";

function cetak() {
  let nama = "Lokal";  // 'Shadow' variabel global
  console.log(nama);   // "Lokal" (menggunakan yang terdekat)
}

cetak();
console.log(nama);     // "Global" (variabel global tidak berubah)

// Mengakses variabel global yang di-shadow
let color = "blue";

function warna() {
  let color = "red";       // Shadow global
  console.log(color);      // "red"
  console.log(window.color); // "blue" (akses via window, di browser)
}
warna();

7. Closure

Closure adalah ketika sebuah fungsi "mengingat" dan bisa mengakses variabel dari scope di mana ia dibuat, bahkan setelah scope tersebut sudah selesai dieksekusi. Ini terjadi karena JavaScript mempertahankan Lexical Environment dari fungsi yang mengacu ke variabel luar. Closure adalah salah satu konsep paling powerful dan penting di JavaScript β€” menjadi dasar dari banyak pattern seperti factory function, module, currying, dan callback.

JavaScript β€” Closure Dasar
// CONTOH 1: Closure paling sederhana
function buatSalam(salam) {
  // 'salam' adalah variabel di outer scope
  
  return function(nama) {
    // Fungsi ini "menutup" (closes over) variabel 'salam'
    return `${salam}, ${nama}!`;
  };
}

const halo = buatSalam("Halo");
const selamat = buatSalam("Selamat pagi");

console.log(halo("Budi"));     // "Halo, Budi!"
console.log(selamat("Andi"));  // "Selamat pagi, Andi!"
// buatSalam sudah selesai eksekusi, tapi 'salam' tetap hidup

// CONTOH 2: Counter dengan closure
function buatCounter(initial = 0) {
  let count = initial;  // Variabel "tertutup" oleh closure
  
  return {
    increment() { return ++count; },
    decrement() { return --count; },
    getCount()  { return count; },
    reset()     { count = initial; return count; }
  };
}

const counter = buatCounter(10);
console.log(counter.increment());  // 11
console.log(counter.increment());  // 12
console.log(counter.decrement());  // 11
console.log(counter.reset());      // 10
// console.log(count);  // ❌ count tidak bisa diakses langsung!

// CONTOH 3: Private variables dengan closure
function buatBankAccount(saldoAwal) {
  let saldo = saldoAwal;  // Private β€” tidak bisa diakses dari luar
  
  return {
    tarik(jumlah) {
      if (jumlah > saldo) {
        console.log("Saldo tidak cukup!");
        return false;
      }
      saldo -= jumlah;
      console.log(`Berhasil tarik Rp${jumlah.toLocaleString()}. Sisa: Rp${saldo.toLocaleString()}`);
      return true;
    },
    setor(jumlah) {
      saldo += jumlah;
      console.log(`Berhasil setor Rp${jumlah.toLocaleString()}. Saldo: Rp${saldo.toLocaleString()}`);
    },
    cekSaldo() {
      return `Saldo: Rp${saldo.toLocaleString()}`;
    }
  };
}

const rekening = buatBankAccount(100000);
rekening.cekSaldo();         // "Saldo: Rp100.000"
rekening.setor(50000);       // "Berhasil setor Rp50.000. Saldo: Rp150.000"
rekening.tarik(30000);       // "Berhasil tarik Rp30.000. Sisa: Rp120.000"
// rekening.saldo β†’ undefined (private!)
Diagram: Closure Mechanism
buatSalam("Halo") dieksekusi:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Lexical Environment            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚ salam: "Halo"         β”‚      β”‚
β”‚  β”‚ outer: global         β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚         β”‚                       β”‚
β”‚         β–Ό (direferensikan oleh)  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚ return function(nama) β”‚      β”‚
β”‚  β”‚   return `${salam},   β”‚      β”‚
β”‚  β”‚     ${nama}!`         β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

const halo = buatSalam("Halo");
// 'halo' sekarang adalah function yang
// MEMEGANG referensi ke 'salam: "Halo"'
// meskipun buatSalam sudah selesai!

halo("Budi") β†’ "Halo, Budi!"
// Mencari 'salam': tidak ada di dalam
// β†’ Naik ke closure β†’ salam = "Halo" βœ…

8. Praktik Closure

Closure bukan hanya konsep teori β€” ia digunakan dalam banyak pola praktis yang Anda temui sehari-hari dalam pengembangan JavaScript. Berikut beberapa contoh penggunaan closure yang paling umum dan berguna.

JavaScript β€” Closure Patterns
// ============================
// PATTERN 1: Memoization (Cache)
// ============================
function memoize(fn) {
  const cache = {};  // Cache tertutup oleh closure
  
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key] !== undefined) {
      console.log(`[cache hit] ${key}`);
      return cache[key];
    }
    console.log(`[computing] ${key}`);
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const faktorial = memoize(function(n) {
  if (n <= 1) return 1;
  return n * faktorial(n - 1);
});

console.log(faktorial(5));  // [computing] [5] β†’ 120
console.log(faktorial(5));  // [cache hit] [5] β†’ 120 (langsung!)
console.log(faktorial(6));  // [computing] [6] β†’ 720

// ============================
// PATTERN 2: Function Factory
// ============================
function buatValidator(rules) {
  return function(value) {
    const errors = [];
    
    if (rules.required && !value) {
      errors.push("Wajib diisi");
    }
    if (rules.minLength && value.length < rules.minLength) {
      errors.push(`Minimal ${rules.minLength} karakter`);
    }
    if (rules.pattern && !rules.pattern.test(value)) {
      errors.push("Format tidak valid");
    }
    
    return {
      valid: errors.length === 0,
      errors
    };
  };
}

const validateEmail = buatValidator({
  required: true,
  pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
});

const validatePassword = buatValidator({
  required: true,
  minLength: 8
});

console.log(validateEmail("budi@email.com"));
// { valid: true, errors: [] }
console.log(validateEmail("budi"));
// { valid: false, errors: ["Format tidak valid"] }
console.log(validatePassword("123"));
// { valid: false, errors: ["Minimal 8 karakter"] }

// ============================
// PATTERN 3: Event Handler dengan State
// ============================
function buatToggleHandler(initialState = false) {
  let state = initialState;
  
  return function(event) {
    state = !state;
    const el = event.target;
    el.classList.toggle("active", state);
    el.textContent = state ? "ON" : "OFF";
    console.log(`Toggle: ${state}`);
  };
}

// Di HTML:
// <button class="toggle-btn">OFF</button>
// <button class="toggle-btn">OFF</button>
// <button class="toggle-btn">OFF</button>

document.querySelectorAll(".toggle-btn").forEach(btn => {
  // Setiap tombol punya state tersendiri via closure
  btn.addEventListener("click", buatToggleHandler());
});

// ============================
// PATTERN 4: Currying
// ============================
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

const tambah = curry((a, b, c) => a + b + c);

console.log(tambah(1)(2)(3));      // 6
console.log(tambah(1, 2)(3));       // 6
console.log(tambah(1)(2, 3));       // 6
console.log(tambah(1, 2, 3));       // 6

// Currying berguna untuk membuat specialized functions
const tambahSepuluh = tambah(10);
console.log(tambahSepuluh(5)(3));   // 18

9. Common Pitfalls

Closure sangat powerful, tapi ada beberapa gotcha yang sering mengecoh developer β€” terutama yang berkaitan dengan loop, memory leak, dan referensi yang tidak disengaja. Memahami pitfall ini akan menyelamatkan Anda dari banyak debugging yang frustasi.

JavaScript β€” Common Pitfalls
// ============================
// PITFALL 1: Closure dalam Loop (klasik!)
// ============================

// ❌ BUG: Semua callback merujuk ke 'i' yang sama
function buatButtonsBug() {
  for (var i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(`Button ${i}`);  // Semua cetak "Button 5"
    }, 100);
  }
}
buatButtonsBug();  // "Button 5" x5 ← BUG!

// βœ… FIX 1: Gunakan let (block scope)
function buatButtonsFix1() {
  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(`Button ${i}`);  // 0, 1, 2, 3, 4
    }, 100);
  }
}

// βœ… FIX 2: IIFE untuk membuat closure per iterasi
function buatButtonsFix2() {
  for (var i = 0; i < 5; i++) {
    (function(index) {
      setTimeout(() => {
        console.log(`Button ${index}`);  // 0, 1, 2, 3, 4
      }, 100);
    })(i);
  }
}

// ============================
// PITFALL 2: Memory Leak
// ============================
// Closure memegang referensi ke Lexical Environment
// Jika closure hidup lama, variabel di dalamnya tidak bisa di-garbage-collect

function buatHeavyData() {
  const dataBaru = new Array(1000000).fill("data");  // Besar!
  
  return function() {
    // Hanya menggunakan dataBaru.length
    return dataBaru.length;
  };
}

let getter = buatHeavyData();
console.log(getter());  // 1000000
// dataBaru TIDAK bisa di-garbage-collect karena closure mereferensikannya
// βœ… FIX: Set getter = null jika sudah tidak digunakan
getter = null;  // Sekarang dataBaru bisa di-gc

// ============================
// PITFALL 3: Shared Reference
// ============================
function buatFunctions() {
  let count = 0;
  
  return {
    // Semua method berbagi 'count' yang SAMA
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
}

const fn = buatFunctions();
console.log(fn.increment());  // 1
console.log(fn.increment());  // 2
console.log(fn.decrement());  // 1 ← Mengubah count yang sama!

// ============================
// PITFALL 4: this dalam Closure
// ============================
const obj = {
  nama: "BeebaneLabs",
  delayGreet() {
    // ❌ BUG: arrow function mengambil this dari outer scope
    setTimeout(() => {
      console.log(this.nama);  // "BeebaneLabs" βœ… (arrow OK di sini)
    }, 100);
    
    // ❌ BUG: function biasa punya this sendiri
    setTimeout(function() {
      console.log(this.nama);  // undefined ❌
    }, 100);
  }
};

// Arrow function mengikuti this dari lexical scope (yang benar!)
// Function biasa punya this berdasarkan bagaimana ia dipanggil

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Scope dan Closure:

Pertanyaan 1: Apa output dari kode berikut?
if (true) { var x = 10; let y = 20; }
console.log(x);
console.log(y);

a) 10 lalu 20
b) 10 lalu ReferenceError
c) undefined lalu ReferenceError
d) ReferenceError untuk keduanya

Pertanyaan 2: Apa itu "closure" dalam JavaScript?

a) Fungsi yang tidak bisa dipanggil
b) Fungsi yang mengingat dan mengakses variabel dari scope luarnya, bahkan setelah scope itu selesai
c) Variabel yang dideklarasi dengan var
d) Cara mengenkripsi data di JavaScript

Pertanyaan 3: Output dari:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

a) 0, 1, 2
b) 3, 3, 3
c) undefined, undefined, undefined
d) Error

Pertanyaan 4: Apa yang dimaksud dengan "hoisting"?

a) Memindahkan variabel ke fungsi lain
b) Deklarasi variabel dan fungsi "dinaikkan" ke atas scope sebelum eksekusi
c) Menghapus variabel dari memory
d) Membuat variabel menjadi konstan

Pertanyaan 5: Mengapa let lebih aman dari var di dalam loop?

a) let lebih cepat dari var
b) let tidak bisa diakses di dalam loop
c) let menciptakan binding baru per iterasi (block scope), var berbagi satu binding
d) let otomatis menggunakan closure
πŸ” Zoom
100%
🎨 Tema