Category: guide
JavaScript Closures และ Scope — เข้าใจจริง ไม่ใช่แค่ท่อง
Scope chain, lexical environment, closures ทำงานยังไง, และ use cases จริงที่เจอบ่อย
สารบัญ
Scope คืออะไร
Scope กำหนดว่า variable ไหนเข้าถึงได้จาก context ไหน JavaScript มี 3 ระดับ:
// Global scope — เข้าถึงได้ทุกที่
const APP_NAME = 'My App';
function outer() {
// Function scope — เข้าถึงได้เฉพาะใน outer()
const x = 10;
if (true) {
// Block scope — เข้าถึงได้เฉพาะใน if block
const y = 20;
let z = 30;
console.log(x); // ✓ เห็น parent scope
}
console.log(y); // ✗ ReferenceError
}
var ไม่มี block scope — นี่คือเหตุผลหลักที่ต้องใช้ let/const:
if (true) {
var leak = 'visible outside'; // hoisted ขึ้น function scope
let safe = 'block only';
}
console.log(leak); // ✓ "visible outside"
console.log(safe); // ✗ ReferenceError
Scope Chain
เมื่อ JS หา variable จะขึ้น scope ไปเรื่อยๆ จนถึง global:
const a = 1;
function outer() {
const b = 2;
function inner() {
const c = 3;
console.log(a, b, c); // ✓ เห็นทุกตัวผ่าน scope chain
}
inner();
console.log(c); // ✗ ReferenceError — ลงไปไม่ได้
}
Scope chain ถูกกำหนด ณ ตอนที่เขียนโค้ด (lexical) ไม่ใช่ตอน runtime
Lexical Scope vs Dynamic Scope
JavaScript ใช้ lexical scope — ตำแหน่งใน source code กำหนด scope:
const name = 'global';
function greet() {
console.log(name); // อ่านจาก scope ตอนเขียน — "global"
}
function test() {
const name = 'local';
greet(); // เรียก greet แต่ greet ยังเห็น "global" ไม่ใช่ "local"
}
test(); // "global"
Closure คืออะไร
Closure คือ function ที่ “จำ” lexical scope ที่มันถูกสร้างขึ้น แม้ scope นั้นจะ execute เสร็จแล้ว:
function makeCounter() {
let count = 0; // variable นี้ "ติด" อยู่กับ function ข้างใน
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// makeCounter() เสร็จแล้ว แต่ count ยังอยู่เพราะ closure
Use Case 1: Data Privacy
function createBankAccount(initialBalance) {
let balance = initialBalance; // private — ไม่มีทางเข้าถึงจากภายนอกโดยตรง
return {
deposit(amount) {
balance += amount;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance); // undefined — ซ่อนอยู่ใน closure
Use Case 2: Function Factory
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
แต่ละ call ของ multiplier สร้าง closure ใหม่ที่มี factor ของตัวเอง
Use Case 3: Memoization
function memoize(fn) {
const cache = new Map(); // closure เก็บ cache
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log('Computing...');
return n * n;
});
expensiveCalc(10); // "Computing..." → 100
expensiveCalc(10); // (ไม่ log) → 100 จาก cache
Use Case 4: Event Handlers
function setupButtons(items) {
items.forEach((item, index) => {
const btn = document.createElement('button');
btn.textContent = item;
// closure capture ค่า item และ index ณ iteration นี้
btn.addEventListener('click', () => {
console.log(`Clicked: ${item} at index ${index}`);
});
document.body.appendChild(btn);
});
}
setupButtons(['Apple', 'Banana', 'Cherry']);
ปัญหาคลาสสิกกับ var (ก่อน let):
// ❌ var — closure ทุกตัวชี้ไปที่ตัวแปรเดียวกัน
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3 — ไม่ใช่ 0, 1, 2
// ✓ let — แต่ละ iteration มี scope ของตัวเอง
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
Use Case 5: Partial Application
function partial(fn, ...preArgs) {
return function (...laterArgs) {
return fn(...preArgs, ...laterArgs);
};
}
function add(a, b, c) { return a + b + c; }
const add10 = partial(add, 10);
console.log(add10(5, 3)); // 18
// ใช้กับ API calls
const getUser = (baseUrl, userId) => fetch(`${baseUrl}/users/${userId}`);
const getLocalUser = partial(getUser, 'http://localhost:3000');
getLocalUser(42); // fetch http://localhost:3000/users/42
Hoisting
var declarations ถูก hoist (ยก) ขึ้นบน function scope — แต่ค่ายังเป็น undefined:
console.log(x); // undefined — ไม่ใช่ ReferenceError
var x = 5;
console.log(x); // 5
let/const ก็ถูก hoist แต่อยู่ใน “Temporal Dead Zone” จนถึงบรรทัดที่ประกาศ:
console.log(y); // ✗ ReferenceError: Cannot access 'y' before initialization
let y = 5;
Functions declarations ถูก hoist ทั้งหมด — เรียกก่อนประกาศได้:
greet(); // "Hello" — ใช้งานได้
function greet() {
console.log('Hello');
}
แต่ function expressions ไม่ได้:
greet(); // ✗ TypeError: greet is not a function
var greet = function () { console.log('Hello'); };
Memory Leak จาก Closure
Closure ป้องกัน garbage collection ของตัวแปรที่ถูก reference — ถ้าไม่ระวัง:
function setup() {
const largeArray = new Array(1_000_000).fill(0); // 8MB
// ❌ closure ที่ไม่จำเป็นทำให้ largeArray อยู่ใน memory ตลอด
document.addEventListener('click', () => {
console.log(largeArray.length);
});
}
// ✓ ถ้าไม่ต้องการ largeArray แล้ว ให้ remove event listener
function teardown(handler) {
document.removeEventListener('click', handler);
}
สรุป Pattern
| Concept | ทำอะไรได้ |
|---|---|
| Closure | เก็บ state โดยไม่ต้องใช้ global variable |
| Function Factory | สร้าง functions ที่ configured มาแล้ว |
| Memoization | cache ผลลัพธ์ที่คำนวณแล้ว |
| Module Pattern | สร้าง private state แบบ OOP-lite |
| Partial Application | lock บาง arguments ของ function ไว้ |