บทนำ
Scope และ Closure ไม่ใช่แค่กฎการเขียนโค้ด แต่มันคือ กลไกการบริหารจัดการหน่วยความจำ (Memory Management) ของภาษา JavaScript (และ TypeScript) การเข้าใจเรื่องนี้อย่างลึกซึ้งจะทำให้คุณเขียนโค้ดที่มีประสิทธิภาพและหลีกเลี่ยง Memory Leak ได้
1. Execution Context & Call Stack
ทุกครั้งที่ฟังก์ชันถูกเรียก (Invoked) JS Engine จะสร้าง Execution Context ขึ้นมา 2 ส่วน:
- Memory Component (Variable Environment): เก็บค่าตัวแปรและฟังก์ชัน
- Code Component (Thread of Execution): รันคำสั่งทีละบรรทัด
เมื่อฟังก์ชันทำงานเสร็จ Context จะถูกทำลาย (Popped off stack) และ Memory ก็ควรจะถูกเคลียร์... ยกเว้นกรณีพิเศษที่เรียกว่า Closure
2. Deep Dive into Closures
Closure เกิดขึ้นเมื่อฟังก์ชันลูก (Inner Function) "อ้างอิง" ตัวแปรจากฟังก์ชันแม่ (Outer Function)
function outer() {
let count = 0; // ตัวแปรนี้ควรจะตายเมื่อ outer() จบ
function inner() {
count++; // แต่อ้างอิงถึง count
console.log(count);
}
return inner;
}
const myFunc = outer(); // outer จบการทำงานแล้ว
myFunc(); // 1
myFunc(); // 2
เกิดอะไรขึ้นเบื้องหลัง? (Under the hood)
ปกติเมื่อ outer() จบ ตัวแปร count ต้องโดน Garbage Collected (GC) ทิ้งไป
แต่เนื่องจาก inner ยังถูกส่งออกไปข้างนอก (return) และมันยัง "ผูกพัน" (Bound) อยู่กับ count
JS Engine (เช่น V8) จะฉลาดพอที่จะ ไม่ทำลาย count แต่จะเก็บมันไว้ใน Heap Memory พิเศษที่เรียกว่า "Closure Scope" หรือบางที่เรียก [[Scopes]] property
นี่คือสาเหตุที่ Closure กิน Memory มากกว่าฟังก์ชันปกติ
3. Lexical Scope vs Dynamic Scope
JavaScript ใช้ Lexical Scope (Static Scope) แปลว่า: "ตำแหน่งที่คุณวางโค้ด คือสิ่งที่กำหนด Scope" ไม่ใช่ตำแหน่งที่คุณเรียกใช้ฟังก์ชัน
let name = "Global";
function sayName() {
console.log(name);
}
function dummy() {
let name = "Local";
sayName(); // เรียก sayName ที่นี่
}
dummy();
// Output: "Global" (ไม่ใช่ Local!)
ทำไม? เพราะตอน sayName ถูกประกาศ (Defined) มันอยู่ข้างนอก มันจึงจำ Scope ข้างนอก (Lexical Environment) ติดตัวไปตลอดกาล ไม่ว่าใครจะเป็นคนเรียกมันก็ตาม
4. let, const และ Block Scope (The TDZ)
ทำไม var ถึงแย่? เพราะ var ผูกกับ Function Scope เท่านั้น ไม่สนใจ { }
if (true) {
var x = 10;
}
console.log(x); // 10 (ทะลุออกมาได้เฉย)
ใน TypeScript/Modern JS เราใช้ let/const ซึ่งทำงานในระดับ Block Scope
เบื้องหลังคือ JS Engine จะสร้าง Zone พิเศษที่เรียกว่า Temporal Dead Zone (TDZ) ตั้งแต่เริ่ม Block จนถึงบรรทัดที่ประกาศตัวแปร ถ้าพยายามเรียกใช้ก่อนหน้านั้น Engine จะปา Error ยับยั้งทันที
สรุป
- Closure คือการที่ฟังก์ชัน "หอบหิ้ว" ข้อมูล (Backpack) ของ Scope แม่ติดตัวไปด้วย
- ข้อมูลใน Closure จะไม่ถูก Garbage Collected ตราบใดที่ฟังก์ชันนั้นยังถูกใช้งาน
- Lexical Scope คือการดูตำแหน่งการเขียนโค้ด ไม่ใช่ตำแหน่งการเรียก
- การใช้
let/constช่วยจัดการ Scope ให้แคบลงและปลอดภัยขึ้น ลดความเสี่ยง Memory Leak จาก Global variables