JavaScript
Functions
Closure
เรียนรู้ว่า Closure คืออะไร ทำไม inner function จึงจำตัวแปรจาก outer scope ได้ และนำไปใช้สร้าง private state กับ function factory
Closure คืออะไร
Closure คือฟังก์ชันที่ยังจำตัวแปรจาก outer scope ได้ แม้ว่า outer function จะ return จบไปแล้ว คิดง่าย ๆ เหมือนกระเป๋า — ทุกครั้งที่สร้างฟังก์ชันในอีกฟังก์ชันหนึ่ง ฟังก์ชันด้านในจะพกตัวแปรของฟังก์ชันด้านนอกติดตัวไปด้วยเสมอ จุดสำคัญ: closure ไม่ได้ดูว่าฟังก์ชันถูกเรียกเมื่อไร แต่มันดูว่าฟังก์ชันถูก**สร้าง**ที่ไหน
inner function ยังใช้ `greeting` ได้ แม้ `makeGreeter` จะ return จบไปแล้ว
function makeGreeter(greeting) {
return function (name) {
return greeting + name;
};
}
const sayHello = makeGreeter("สวัสดี ");
console.log(sayHello("Mali")); // "สวัสดี Mali"
console.log(sayHello("Nok")); // "สวัสดี Nok"`sayHello` คือ closure — มันจำค่า `greeting = "สวัสดี "` จากตอนที่ `makeGreeter` ถูกเรียก และใช้ค่านั้นซ้ำได้ตลอด โดยไม่ต้องส่ง `greeting` เข้ามาใหม่ทุกครั้ง
Lexical Scope — Closure จำค่าได้เพราะมองจากจุดที่ประกาศ
สาเหตุที่ closure ทำงานได้แบบนี้ เพราะ JavaScript ใช้ **lexical scope** ความหมายตรง ๆ คือ: ฟังก์ชันมองเห็นตัวแปรตามจุดที่มันถูก**เขียน**ไว้ในโค้ด ไม่ใช่จุดที่ถูกเรียกใช้
แม้ภายนอกจะมี `greeting` อีกตัว แต่ inner function จะใช้ `greeting` จาก `outer()` เพราะนั่นคือ scope ที่มันถูกประกาศ
const greeting = "global";
function outer() {
const greeting = "local";
return function () {
return greeting;
};
}
const readGreeting = outer();
console.log(readGreeting()); // "local" — ไม่ใช่ "global"กฎง่าย ๆ: inner function จะใช้ตัวแปรจาก scope ที่ใกล้ตัวมันที่สุดก่อน ถ้าไม่เจอจึงค่อยไล่ขึ้นไป — ตรงกับหลัก scope chain ที่เรียนในบทก่อน
Function Factory — ใช้ Closure สร้างฟังก์ชันที่จำค่าตั้งต้นไว้
หนึ่งในวิธีใช้ closure ที่พบบ่อยที่สุด คือ **function factory** — ฟังก์ชันที่สร้างฟังก์ชันอีกที โดยแต่ละตัวจะพกค่าตั้งต้นของตัวเองไปด้วย
`add10` จำ `base = 10` — เรียกใช้ทีหลังก็ยังใช้ค่านั้นได้
function makeAdder(base) {
return function (number) {
return base + number;
};
}
const add10 = makeAdder(10);
const add100 = makeAdder(100);
console.log(add10(5)); // 15
console.log(add10(20)); // 30
console.log(add100(1)); // 101`add10` และ `add100` เป็น closure คนละตัว — แต่ละตัวจำ `base` ของตัวเองแยกจากกัน
สร้าง checker หลายตัว แต่ละตัวใช้เกณฑ์ของตัวเอง
function makeScoreChecker(passScore) {
return function (score) {
return score >= passScore;
};
}
const isPassed = makeScoreChecker(50);
const isHonor = makeScoreChecker(80);
console.log(isPassed(60)); // true
console.log(isPassed(35)); // false
console.log(isHonor(85)); // truePattern นี้เรียกว่า **function factory** — ฟังก์ชันที่รับค่าตั้งต้น แล้วคืนฟังก์ชันที่นำค่านั้นไปใช้เมื่อถูกเรียกทีหลัง
State ภายใน — Closure ใช้เก็บข้อมูลที่จำได้ข้ามการเรียก
Closure ไม่ได้จำแค่ค่าตั้งต้นที่รับมาตอนแรก — แต่มันยังจำค่าที่**เปลี่ยนแปลงระหว่างการเรียก**ได้ด้วย ทำให้เราสร้างฟังก์ชันที่มี state ภายในของตัวเองได้
ทุกครั้งที่เรียก `counter()` ค่า `count` จะถูกเพิ่มต่อจากครั้งก่อน ไม่ได้เริ่มใหม่
function makeCounter() {
let count = 0;
return function () {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3**แต่ละ closure มี state ของตัวเอง** — ถ้าเรียก `makeCounter()` ซ้ำอีกครั้ง จะได้ counter ตัวใหม่ที่เริ่มนับจาก 1 ใหม่ ไม่ได้ใช้ `count` ร่วมกับตัวเก่า
ข้อควรระวัง: Closure ใน Loop กับ `var`
เมื่อใช้ closure ใน `for` loop กับ `var` — callback ทุกตัวจะอ้างถึงตัวแปรเดียวกัน จึงเห็นค่าสุดท้ายเหมือนกันหมด เพราะ `var` ไม่มี block scope ตัวแปร `i` จึงมีเพียงตัวเดียวที่ถูกอัปเดตทุกครั้งที่วน loop
fnA กับ fnB return 2 เหมือนกันทั้งคู่ เพราะ `var i` มีเพียงตัวเดียว ทุกฟังก์ชันใน loop อ้างถึง `i` ตัวเดียวกัน
var fnA, fnB;
for (var i = 0; i < 2; i++) {
if (i === 0) fnA = function () { return i; };
if (i === 1) fnB = function () { return i; };
}
console.log(fnA()); // 2 (ไม่ใช่ 0 อย่างที่คิด)
console.log(fnB()); // 2`let` สร้าง binding ใหม่ในแต่ละรอบ loop ทำให้แต่ละ closure จำค่า `j` ของรอบตัวเอง
var fnX, fnY;
for (let j = 0; j < 2; j++) {
if (j === 0) fnX = function () { return j; };
if (j === 1) fnY = function () { return j; };
}
console.log(fnX()); // 0
console.log(fnY()); // 1**กฎ**: เมื่อใช้ closure ใน loop ให้ใช้ `let` เสมอ — มันจะสร้างตัวแปรใหม่ในแต่ละรอบ loop ซึ่งเป็นสิ่งที่ closure ต้องการเพื่อจำค่าที่ถูกต้อง
ข้อผิดพลาดที่พบบ่อยกับ Closure
| สิ่งที่พลาดบ่อย | ผลที่เกิดขึ้น | วิธีแก้ |
|---|---|---|
| ใช้ `var` ใน loop แล้วสร้าง closure | ทุก closure เห็นค่าเดียวกัน | เปลี่ยน `var` เป็น `let` |
| ลืม return inner function | outer function return undefined | ต้อง return inner function (ไม่ใช่เรียกมัน) |
| คิดว่า closure แชร์ state กัน | คาดว่า counter ทุกตัวนับร่วมกัน | แต่ละการเรียก outer function จะสร้าง closure คนละชุด |
| ใช้ arrow function เป็น inner function โดยลืม `return` | inner function return undefined | ถ้าใช้ `{}` ต้องเขียน `return` เอง |
**สิ่งที่ควรจำจากบทนี้**: Closure = ฟังก์ชัน + ตัวแปรที่มันจำได้จาก scope ที่ถูกสร้าง มันคือวิธีที่ JavaScript ทำให้ฟังก์ชันมี 'ความทรงจำ' — ใช้เมื่ออยากให้ฟังก์ชันพกค่าติดตัวไปใช้ทีหลัง โดยไม่ต้องส่งเป็น argument ทุกครั้ง