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 Scope | Kode di luar fungsi/blok | Diakses dari mana saja | var, let, const |
| Function Scope | Fungsi | Hanya di dalam fungsi | var, let, const |
| Block Scope | {} (if, for, dll) | Hanya di dalam blok | let, const saja |
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.
// ============================
// 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.
// ============================
// 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);
console.log(x);
console.log(y);