Scope + Closure
function/block scope, lexical scope, closure ที่ inner function จำตัวแปร outer ไว้ได้ เป็นรากของ React Hooks และ pattern modern ทั้งหมด
โค้ดนี้ error ทายได้ไหมว่าทำไม
function greet() {
const name = "Top"
}
greet()
console.log(name) // ReferenceError: name is not definedคำตอบคือ name อยู่ใน scope ของ greet เข้าถึงได้แค่ในนั้น นอกจากนั้นเข้าไม่ถึง scope คือขอบเขตการมองเห็นของ ตัวแปร บทนี้จะเข้าใจ scope กับ concept สำคัญของ JS ที่เรียกว่า closure
Scope 2 ชนิด
- Function scope คือตัวแปรที่ประกาศในฟังก์ชัน เข้าถึงได้แค่ในฟังก์ชันนั้น
- Block scope คือตัวแปร
let/constที่ประกาศใน{ }(if/for/while block) เข้าถึงได้แค่ใน block นั้น
function demo() {
const a = 1 // function scope
if (true) {
const b = 2 // block scope
console.log(a, b) // 1 2 ✓
}
console.log(a) // 1 ✓
console.log(b) // ❌ ReferenceError เพราะ b อยู่ใน block
}Lexical scope อ่านจาก code ตอนเขียน
Scope ใน JS เป็น lexical แปลว่า รู้จากโครงสร้าง code ตอนเขียน ไม่ใช่ตอนรัน คือฟังก์ชันซ้อนกันยังไง scope ก็ซ้อนกันแบบนั้น
ลองคลิกดูว่าตอนยืนอยู่ scope ไหน เห็นตัวแปรอะไรได้บ้าง
const x = 1const y = 2const z = 3xyzClosure ของลับของ JS
ต่อยอดจาก scope closure คือปรากฏการณ์ที่ inner function จำตัวแปรของ outer function ไว้ได้ แม้ outer จะ return ไปแล้ว
ฟังดูเหมือนเวทมนตร์ ลองดู
function createGreeter(name) {
return function() {
console.log(`สวัสดี ${name}!`)
}
}
const greetTop = createGreeter("Top")
greetTop() // "สวัสดี Top!" ← name ยังอยู่!ตามหลักการ name อยู่ใน scope ของ createGreeter เมื่อฟังก์ชันนี้ return เสร็จแล้ว scope ของมันควรหายไป แต่ name ยังอยู่ เพราะ inner function ที่ return ออกมา เก็บ มันเอาไว้
Closure ใน action: Factory pattern
Factory function คือฟังก์ชันที่สร้าง function อื่นๆ ออกมา แต่ละตัวที่สร้างมี state แยก ใช้ closure ทำ
function createCounter() {
let count = 0 // private closure จับไว้
return () => {
count++
return count
}
}
const counter1 = createCounter()
const counter2 = createCounter()
// 2 counter มี count แยกกันคนละตัวกดปุ่มสร้าง counter ตัวแรก
count ควรหายไปหลัง createCounter return เสร็จ เพราะอยู่ใน scope ของฟังก์ชัน แต่กลับ ไม่หาย เพราะ inner function (ที่ return ออกมา) จับ count ไว้ นี่คือ closureที่ไหนใช้ closure ในโลกจริง
- React Hooks เช่น
useState,useEffectทุกตัว พึ่ง closure ในการจำ state ระหว่าง re-render - Event handler เช่น
button.onclick = () => { ... }arrow function ที่จับ variable ของ scope ข้างนอก - setTimeout / setInterval callback ที่ต้องจำ state ตอนตั้ง timer
- Module pattern ซ่อน implementation ไว้ข้างใน expose แค่ API
ตัวอย่าง: useState pattern
ลองเขียน useState แบบของตัวเองด้วย closure จะเข้าใจ React ง่ายขึ้นเยอะ
function useState(initial) {
let value = initial // closure จับไว้
const get = () => value
const set = (newValue) => {
value = newValue
}
return [get, set]
}
const [getCount, setCount] = useState(0)
console.log(getCount()) // 0
setCount(5)
console.log(getCount()) // 5ของจริงใน React ซับซ้อนกว่านี้ (เพราะต้อง trigger re-render) แต่แก่นคือตรงนี้ closure จำ value ระหว่างการ call
กับดักของ closure ที่พลาดบ่อย
Pattern คลาสสิกที่ตอบผิดบ่อย
// ต้องการ: ปริ้น 1, 2, 3
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100)
}
// ปริ้น: 4, 4, 4 ❌เพราะ var เป็น function scope ทำให้ i เป็นตัวเดียวกันทั้ง loop callback ใน setTimeout ทำงานหลัง loop จบ ตอนนั้น i เป็น 4 ไปแล้ว
// แก้ด้วย let (block scope)
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100)
}
// ปริ้น: 1, 2, 3 ✓let สร้าง i ใหม่ทุก iteration closure ของแต่ละ callback จับ i คนละตัว
สรุป
- Function scope คือตัวแปรในฟังก์ชัน เข้าถึงได้แค่ในนั้น
- Block scope คือตัวแปร
let/constใน{ } - Lexical scope คือ inner เห็น outer, outer ไม่เห็น inner ตามที่เขียน
- Closure คือ inner function จับตัวแปรของ outer ไว้ได้แม้ outer return ไปแล้ว
- Closure เป็นรากของ useState, factory function, private state
- ใช้
letใน loop เสมอ ถ้าใช้varจะเจอกับดัก closure
บทสุดท้ายของ topic นี้คือ async กับ event loop จะอธิบายว่าทำไม JS เป็น single-thread แต่ทำหลายอย่างพร้อมกันได้ และ setTimeout กับ Promise ใครชนะใน race