☀️Siang
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

Artikel: Javascript Closures Scope Artikel: Javascript Closures Scope


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: GLOBAL SCOPE

FUNCTION SCOPE function myFunc() ( var a = 1; l...

BLOCK SCOPE if (true) ( let c = 3; const d = 4;...

for ("let i = 0; i 10; i++") ( // i hanya di sin...



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.

flowchart TD
    N0["buatSalam("'Halo'") dieksekusi:"]
    N1["return function(nama) return `$(salam), $(nama)!`"]
    N2["Lexical Environment salam: 'Halo' outer: global"]
    N0 --> N2
    N2 --> N1


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