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 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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.
// 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"
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.
// 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.
// 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!)
}
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.
// 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
// 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.
// 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.
// 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!)
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.
// ============================
// 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);