โหมดมืด
บทที่ 05 — Function เชิงลึก: Closure และ this
บทนี้คือ บทที่สำคัญที่สุดของ JavaScript — เพราะ JavaScript เป็น "ภาษาที่ function เป็นพระเอก"
มี 3 หัวข้อที่ทุกคนต้องเข้าใจให้ลึก — และเป็น 3 หัวข้อที่ถูกถามในการสัมภาษณ์งานบ่อยที่สุด:
- Closure — function ที่ "จำ" สภาพแวดล้อมของมันได้
this— ตัวแปรพิเศษที่เปลี่ยนความหมายตามวิธีเรียก- Arrow function — function แบบใหม่ที่ต่างจากแบบเก่า
ถ้าเข้าใจ 3 เรื่องนี้ — คุณเข้าใจ JavaScript ไปแล้วครึ่งหนึ่ง
ใช้เวลา 3-4 ชั่วโมง — บทนี้ยาก อ่านช้า ๆ ลองโค้ดทุกตัวอย่าง
Part 1: Function คืออะไร และทำไมสำคัญ
1.1 ทบทวน
Function = ชุดคำสั่งที่ตั้งชื่อไว้ เรียกใช้ซ้ำได้
javascript
function greet(name) {
return "สวัสดี " + name;
}
greet("Alice"); // "สวัสดี Alice"
greet("Bob"); // "สวัสดี Bob"name= parameter (ตัวแปรรับค่า)"Alice"ตอนเรียก = argument (ค่าที่ส่งเข้าไป)return= ส่งค่ากลับออกมา
1.2 JavaScript — function เป็น "ค่า" ชนิดหนึ่ง (first-class citizen)
นี่คือแนวคิดสำคัญ — ใน JavaScript function เป็น "ค่า" ชนิดหนึ่ง ใช้ได้ทุกที่ที่ค่าอื่นใช้ได้ เช่น เก็บใส่ตัวแปร ส่งต่อ หรือคืนจากฟังก์ชันอื่น ศัพท์ฝรั่งเรียกแนวคิดนี้ว่า first-class citizen (แปลว่า "สิทธิ์เท่าเทียมกับค่าทุกชนิด")
หมายความว่า function ทำสิ่งที่ค่าทั่วไปทำได้ทั้งหมด:
javascript
// 1. เก็บ function ใส่ตัวแปรได้
const sayHi = function() { return "Hi"; };
// 2. ส่ง function เป็น argument ให้ function อื่นได้
[1, 2, 3].forEach(function(n) { console.log(n); });
// 3. function คืน function ได้
function makeAdder(x) {
return function(y) { return x + y; };
}ความสามารถนี้คือรากฐานของ closure และ functional programming — สำคัญมาก
Part 2: 5 วิธีสร้าง Function
JavaScript มีหลายวิธีสร้าง function — ต้องรู้จักทุกแบบ
2.1 Function Declaration
javascript
function add(a, b) {
return a + b;
}ลักษณะพิเศษ — มัน "hoisted" (hoisting = "การยกขึ้นบนสุด" — JS อ่านและจำ function ทั้งหมดก่อน แล้วค่อยรัน) = เรียกใช้ก่อนบรรทัดที่ประกาศได้:
💡 Hoisting ต่างจาก TDZ ของ let/const (จำบทที่ 02 ได้ไหม): function declaration ถูก "ยกทั้งตัว" ขึ้นมาก่อนรัน — ใช้ได้ทันทีทุกที่ในไฟล์ ในขณะที่
let/constถูกยักขึ้นแค่ชื่อ แต่ยังไม่ได้กำหนดค่า ทำให้มี TDZ (Temporal Dead Zone — เขตห้ามชั่วคราว ก่อนบรรทัดประกาศ)
javascript
sayHello(); // ✅ ใช้ได้ แม้ประกาศข้างล่าง
function sayHello() {
console.log("Hello");
}JavaScript "ยก" function declaration ขึ้นไปไว้บนสุดก่อนรัน — เลยเรียกก่อนได้
2.2 Function Expression
javascript
const add = function(a, b) {
return a + b;
};คือการ "เก็บ function ใส่ตัวแปร" — ต่างจาก declaration ตรงที่ ไม่ hoisted:
javascript
sayHello(); // ❌ Error — ใช้ก่อนประกาศไม่ได้
const sayHello = function() {
console.log("Hello");
};2.3 Arrow Function (ES6 — สมัยใหม่)
javascript
const add = (a, b) => {
return a + b;
};arrow function เขียนสั้นได้หลายแบบ:
javascript
// เต็มรูปแบบ
const add = (a, b) => { return a + b; };
// ถ้า body มีบรรทัดเดียวที่ return — ตัด { } และ return ออกได้
const add = (a, b) => a + b;
// ถ้ามี parameter ตัวเดียว — ตัดวงเล็บออกได้
const square = x => x * x;
// ถ้าไม่มี parameter — ต้องมีวงเล็บว่าง
const getRandom = () => Math.random();
// คืน object — ต้องครอบด้วยวงเล็บ (ไม่งั้น { } ถูกมองเป็น block)
const makeUser = name => ({ name: name });arrow function ไม่ใช่แค่ "เขียนสั้น" — มันมีความต่างสำคัญเรื่อง this (Part 5)
2.4 Method ใน Object
เมื่อ function ถูกเก็บเป็น property ของ object เราเรียกมันว่า "method" — JavaScript มีรูปย่อ (method shorthand) ให้เขียนสั้นลงโดยไม่ต้องใส่ : function:
javascript
const calculator = {
add(a, b) { // method shorthand
return a + b;
},
};
calculator.add(2, 3); // 52.5 IIFE — function ที่เรียกตัวเองทันที
javascript
(function() {
console.log("รันทันทีเลย");
})();IIFE (Immediately Invoked Function Expression — "ฟังก์ชันที่เรียกใช้ทันทีที่สร้าง") = "สร้าง function แล้วเรียกทันที" — สมัยก่อน (ยุค ES5 ก่อนปี 2015) โค้ดใช้ IIFE เพื่อ "ซ่อน" ตัวแปรไม่ให้ชนกับสคริปต์อื่นในหน้าเว็บเดียวกัน เหตุผลคือตอนนั้น JavaScript ยังไม่มี let/const (มีแต่ var ที่ไม่เคารพ block) และยังไม่มีระบบ module มาตรฐานให้แบ่งไฟล์ (ES Modules ยังไม่มี — CommonJS ของ Node.js มีมาก่อน แต่ใช้ในเบราว์เซอร์ตรง ๆ ไม่ได้)
ปัจจุบันใช้น้อยลงมาก (มี module แทน — บทที่ 10) แต่ยังพบได้ในโค้ดสมัยใหม่บางส่วน เช่น async IIFE สำหรับรัน async ใน environment ที่ไม่รองรับ top-level await:
javascript
// async IIFE — ยังใช้อยู่ในปัจจุบัน
(async () => {
const data = await fetch("...");
console.log(data);
})();เหตุที่มือใหม่ควรรู้จัก IIFE แม้ไม่ค่อยได้ใช้: เพื่ออ่านโค้ดเก่าออก และ เพื่อเข้าใจ pattern ของ closure ที่ห่อ scope
2.6 ⚠️ ระวัง — return ขึ้นบรรทัดใหม่
javascript
function bad() {
return
{ value: 5 }; // ❌ คืน undefined!
}JavaScript ใส่ ; ให้อัตโนมัติหลัง return ที่ไม่มีค่าตามมาในบรรทัดเดียวกัน (ขึ้นบรรทัดใหม่ทันที) → กลายเป็น return; (คืน undefined)
→ อย่าขึ้นบรรทัดใหม่หลัง return:
javascript
function good() {
return {
value: 5
};
}Part 3: Parameter — รับค่าเข้า function
3.1 Default Parameter — ค่าเริ่มต้น
เรากำหนด "ค่าเริ่มต้น" ให้พารามิเตอร์ได้ด้วย = — ถ้าผู้เรียกไม่ส่งค่านั้นมา (หรือส่ง undefined) ฟังก์ชันจะใช้ค่า default แทน ทำให้ฟังก์ชันยืดหยุ่นและกัน error:
javascript
function greet(name = "คนแปลกหน้า", greeting = "สวัสดี") {
return `${greeting} ${name}`;
}
greet(); // "สวัสดี คนแปลกหน้า"
greet("Alice"); // "สวัสดี Alice"
greet("Alice", "หวัดดี"); // "หวัดดี Alice"
greet(undefined, "หวัดดี"); // "หวัดดี คนแปลกหน้า" — ส่ง undefined = ใช้ default3.2 Rest Parameter — รับกี่ตัวก็ได้
จำ rest (...) จากบทที่ 03 ได้ไหม — ใช้ใน parameter เพื่อรับ argument ไม่จำกัดจำนวน:
javascript
function sum(...numbers) { // รวบทุก argument เป็น array
let total = 0;
for (const n of numbers) total += n;
return total;
}
sum(1, 2); // 3
sum(1, 2, 3, 4, 5); // 15...numbers ต้องเป็น parameter ตัวสุดท้ายเสมอ
3.3 Destructuring ใน Parameter
จากบทที่ 03 — แกะค่าจาก object ที่ส่งเข้ามาได้เลย:
javascript
function createUser({ name, age = 0, role = "user" }) {
return { name, age, role };
}
createUser({ name: "Alice", age: 25 });
// { name: "Alice", age: 25, role: "user" }เทคนิคนี้ดีเมื่อ function รับ option หลายตัว — เรียกใช้แล้วอ่านง่าย (เห็นว่าแต่ละค่าคืออะไร)
Part 4: Closure — หัวใจของ JavaScript
นี่คือหัวข้อที่ยากที่สุดและสำคัญที่สุด — ตั้งใจอ่านมาก ๆ
4.1 Closure คืออะไร
Closure = ฟังก์ชันที่ "จำ" ตัวแปรรอบนอกได้ แม้ฟังก์ชันที่สร้างมันจะสิ้นสุดไปแล้ว
Closure = function ที่ "จำ" ตัวแปรในสภาพแวดล้อมที่มันถูกสร้างขึ้นได้ — แม้สภาพแวดล้อมนั้นจะจบไปแล้ว
ฟังดูงง — มาดูตัวอย่าง:
javascript
function makeCounter() {
let count = 0; // ตัวแปรนี้อยู่ใน makeCounter
return function() { // function ข้างในนี้ "จำ" count ได้
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3เกิดอะไรขึ้น — ลองคิดดี ๆ:
- เรียก
makeCounter()— มันสร้างตัวแปรcount = 0แล้ว return function ข้างในออกมา - หลัง
makeCounter()จบ — ปกติcountควรหายไป (เพราะ function จบแล้ว) - แต่มันไม่หาย — เพราะ function ข้างในที่ return ออกมา "ยังจำ
countได้" →countเลยมีชีวิตอยู่ต่อ - ทุกครั้งที่เรียก
counter()— มันเข้าถึงและเพิ่มcountตัวเดิมนั้น
→ "function ข้างใน + ตัวแปรที่มันจำได้" = closure
4.2 แต่ละ closure เป็นอิสระต่อกัน
javascript
const counterA = makeCounter();
const counterB = makeCounter();
counterA(); // 1
counterA(); // 2
counterB(); // 1 — counterB มี count ของตัวเอง แยกจาก counterAแต่ละครั้งที่เรียก makeCounter() — สร้าง count ใหม่ ผูกกับ closure ใหม่
💡 เปรียบเทียบ: closure เหมือน "กระเป๋าเป้" ที่ function สะพายติดตัว — ในกระเป๋ามีตัวแปรที่มันต้องใช้ — function ไปไหน กระเป๋าไปด้วย
4.3 Closure ใช้ทำอะไร — ตัวอย่างจริง
💡 หมายเหตุ: ตัวอย่าง 4.1 (
makeCounter) คืน function เดี่ยว แต่ closure ไม่จำเป็นต้องคืนแค่ function เดียว — สามารถ คืน object ที่มีหลาย method ซึ่งทุก method "จำ" ตัวแปรเดียวกันได้ เห็นได้ในตัวอย่างที่ 1 ด้านล่าง
ใช้ที่ 1: สร้าง "ตัวแปรส่วนตัว" (private state)
JavaScript ไม่มี keyword private สำหรับตัวแปร — แต่ closure ทำให้เกิด private ได้:
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // ตัวแปรส่วนตัว — ข้างนอกแตะไม่ได้
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) {
return "ยอดเงินไม่พอ";
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
account.getBalance(); // 120
account.balance; // undefined — เข้าถึง balance ตรง ๆ ไม่ได้!balance อยู่ใน closure — แก้ได้แค่ผ่าน deposit/withdraw ที่เราอนุญาต — นี่คือ "การห่อหุ้มข้อมูล" (encapsulation)
ใช้ที่ 2: สร้าง function ที่ตั้งค่าไว้ล่วงหน้า
javascript
function makeMultiplier(factor) {
return function(number) {
return number * factor; // จำ factor ได้
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
double(5); // 10
triple(5); // 15double คือ function ที่ "จำว่า factor = 2" — triple จำว่า factor = 3
ใช้ที่ 3: Memoization (จำผลลัพธ์) — ไม่คำนวณซ้ำ
ลองนึกถึงฟังก์ชันที่คำนวณช้า เช่น คิด Fibonacci ตัวที่ 40 ใช้เวลานานหลายวินาที — ถ้าเรียกซ้ำด้วย input เดิม คำนวณใหม่ทุกครั้งเสียเวลามาก memoization = "จำผลลัพธ์ที่เคยคำนวณไว้ ครั้งหน้าคืนค่าเดิมเลย":
javascript
function memoize(fn) {
const cache = {}; // cache (ตู้เก็บผลลัพธ์) อยู่ใน closure
return function(n) {
if (n in cache) {
console.log("เอาจาก cache");
return cache[n];
}
console.log("คำนวณใหม่");
const result = fn(n);
cache[n] = result;
return result;
};
}
// ตัวอย่าง — สมมติว่า square ตัวนี้ "ช้า" (จริง ๆ เร็วมาก ใช้แค่ดูพฤติกรรม memoize)
const square = (n) => n * n;
const fastSquare = memoize(square);
fastSquare(5); // คำนวณใหม่ → 25
fastSquare(5); // เอาจาก cache → 25📦 อ่านข้ามได้ถ้ายังไม่พร้อม — ข้อจำกัดของ memoize เวอร์ชันบทเรียน
เวอร์ชันด้านบนทำให้เห็นไอเดียของ closure — แต่ยังไม่พร้อมใช้งานจริง ถ้ายังเป็นมือใหม่ ข้ามย่อหน้านี้ไปก่อนได้ ค่อยกลับมาอ่านตอนทำโปรเจกต์จริง:
- รับ argument ตัวเดียว — ยังไม่รองรับหลาย argument พร้อมกัน
- ใช้ object ธรรมดาเป็น cache → JavaScript บังคับแปลงชนิด key ให้เป็นข้อความอัตโนมัติ (การแปลงชนิดอัตโนมัติแบบที่เรียนในบทที่ 00/02) ทำให้เลข
1กับข้อความ'1'ใช้ช่องเดียวกัน (key ชนกัน)- ไม่มีการล้าง cache — cache โตขึ้นเรื่อย ๆ จนเป็น "memory leak" (หน่วยความจำรั่ว — โปรแกรมกินเมมเพิ่มเรื่อย ๆ ไม่คืนระบบ)
- งานจริงนิยมใช้
Mapแทน object ธรรมดา และจำกัดขนาด cache ด้วยกลยุทธ์อย่าง LRU (Least Recently Used = "ลบของที่ไม่ได้ใช้นานสุดก่อน") — รายละเอียดเพิ่มเติม (เช่นWeakMap) เรียนตอนทำงานจริง
Part 4.5 — ⚠️ Closure กับ Memory Leak (หน่วยความจำรั่ว) — เรื่องที่ต้องระวังในงานจริง
📌 อ่านเพื่อรู้แนวคิด — ไม่ต้องจำ syntax ตอนนี้: ส่วนนี้แตะ DOM (Document Object Model — โครงสร้างหน้าเว็บ),
addEventListener,AbortControllerซึ่งจะเรียนเต็มในบทที่ 12 ตอนนี้ดูแค่แนวคิด "closure ค้างในเมม" พอ — ถ้ายังงง ข้ามไปได้ก่อน แล้วค่อยกลับมาอ่านหลังเรียนบทที่ 12
Closure "จำ" ตัวแปรนอกได้ — ข้อดี แต่ก็เป็นเหตุของ memory leak (หน่วยความจำรั่ว) ที่พบบ่อยในงานจริง:
- Event listener (ผู้ฟังเหตุการณ์) ที่ลืม remove — listener เก็บ closure ที่อ้างถึง DOM node (ชิ้นส่วนของหน้าเว็บ) → GC (Garbage Collector — ตัวเก็บขยะของ JS) ปลดเมมโมรีไม่ได้แม้ลบ node ไปแล้ว
setIntervalที่ลืมclearInterval— callback (ฟังก์ชันที่ส่งให้ตัวอื่นเรียกทีหลัง) ถูกอ้างไว้โดย timer ตลอด → ตัวแปรทุกตัวใน closure ค้างในเมม- Closure ที่จับ object ใหญ่โดยไม่ตั้งใจ — แม้ใช้แค่ field เดียว แต่ทั้ง object โดน reference ค้าง
Pattern cleanup (โค้ดตัวอย่างนี้ใช้ AbortController/signal/DOM — ถ้ายังไม่คุ้น ข้ามไปได้เลย แล้วย้อนกลับมาอ่านหลังเรียนบทที่ 12):
javascript
// ❌ leak — listener ถ้าไม่ remove → bigData ค้างในเมมตลอดอายุ button
function setup(button, bigData) {
button.addEventListener("click", () => {
console.log(bigData.length); // closure จับ bigData
});
}
// ✅ cleanup ด้วย AbortController — เครื่องมือมาตรฐานของเว็บที่ใช้ "สั่งยกเลิก"
// operation ต่าง ๆ ผ่าน signal ที่ส่งให้ตัวจัดการ (รายละเอียดเต็มในบทที่ 12)
function setup(button, bigData) {
const controller = new AbortController();
button.addEventListener("click", () => {
console.log(bigData.length);
}, { signal: controller.signal }); // ผูก listener กับ signal
return () => controller.abort(); // เรียกเพื่อยกเลิก listener (unsubscribe)
}ใน framework UI (React, Vue, Angular) — เก็บ cleanup function แล้วเรียกตอน unmount/destroy (ตอนคอมโพเนนต์ถูกถอดออกจากหน้าจอหรือถูกทำลาย)
4.4 ⚠️ กับดัก Closure ใน Loop
ปัญหาคลาสสิก — ใช้ var ใน loop กับ closure:
javascript
// ❌ ใช้ var — ผิด
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// ผลลัพธ์: 3, 3, 3 — ไม่ใช่ 0, 1, 2!ทำไม? — var ไม่เคารพ block (จำบทที่ 02 ได้ไหม) → มี i แค่ตัวเดียวที่ทุก closure แชร์ → พอ setTimeout ทำงาน (หลัง loop จบ) i เป็น 3 ไปแล้ว
javascript
// ✅ ใช้ let — ถูก
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// ผลลัพธ์: 0, 1, 2let สร้าง i ใหม่ทุกรอบ loop → แต่ละ closure จำ i ของรอบตัวเอง → ถูกต้อง
→ นี่คืออีกเหตุผลที่ใช้ let/const ไม่ใช่ var
Part 5: this — ตัวแปรที่เปลี่ยนความหมาย
this คือเรื่องที่ทำให้คน "เกลียด" JavaScript มากที่สุด — แต่พอเข้าใจแล้วไม่ยาก
5.1 this คืออะไร
this เป็นตัวแปรพิเศษที่ JavaScript ใส่ให้อัตโนมัติทุก function — ค่าของมันขึ้นกับ "วิธีเรียก" function ไม่ใช่ "ที่ที่เขียน" function
ประโยคนี้สำคัญมาก — อ่านอีกรอบ: this = "ใครเรียก function" ไม่ใช่ "function อยู่ที่ไหน"
5.2 4 กรณีของ this
กรณี 1: เรียกเป็น method ของ object
javascript
const user = {
name: "Alice",
greet() {
return "สวัสดี " + this.name; // this = object ที่เรียก method นี้
},
};
user.greet(); // "สวัสดี Alice" — this คือ userเมื่อเรียก user.greet() — this ภายใน greet คือ user (เพราะ user เป็นคนเรียก)
กรณี 2: เรียกเป็น function เปล่า ๆ
javascript
function show() {
console.log(this);
}
show(); // undefined (ใน strict mode / module) หรือ global objectglobal object คืออะไร: ใน browser คือ window, ใน Node.js คือ globalThis — แต่ใน module mode ที่หนังสือเล่มนี้ใช้ตลอดทั้งเล่ม this ในฟังก์ชันเปล่า ๆ จะได้ undefined เสมอ ไม่ใช่ global object
💡 Strict mode คืออะไร: โหมด "เข้มงวด" ของ JavaScript ที่ดักความผิดพลาดให้มากขึ้น เช่น ห้ามใช้ตัวแปรที่ไม่ได้ประกาศ และทำให้
thisใน function เปล่า ๆ เป็นundefined(โหมดเก่าจะให้เป็น global object) — JavaScript ยุคใหม่เปิด strict mode อัตโนมัติเมื่อใช้ ES Module (ไฟล์ที่ใช้import/export— มาตรฐานปัจจุบัน เรียนเต็มในบทที่ 10) หนังสือเล่มนี้ถือว่าโค้ดทุกตัวอย่างรันใน module mode
เรียก function เปล่า ๆ ไม่มีใคร "เรียก" → this เป็น undefined
กรณี 3: เรียกด้วย call / apply / bind (กำหนด this เอง)
javascript
function greet() {
return "สวัสดี " + this.name;
}
const alice = { name: "Alice" };
greet.call(alice); // "สวัสดี Alice" — บังคับให้ this = aliceกรณี 4: เรียกด้วย new (constructor)
javascript
function User(name) {
this.name = name; // this = object ใหม่ที่ถูกสร้าง
}
const u = new User("Alice"); // this คือ object ใหม่5.3 ปัญหาคลาสสิก — this หายใน callback
นี่คือบั๊กที่มือใหม่เจอบ่อยที่สุด:
javascript
const user = {
name: "Alice",
greetLater() {
setTimeout(function() {
console.log("สวัสดี " + this.name);
// ❌ ใน module/strict mode → this เป็น undefined
// → อ่าน this.name แล้ว throw TypeError: Cannot read properties of undefined
}, 1000);
},
};
user.greetLater();
// → TypeError (ใน module mode ที่หนังสือเล่มนี้ใช้)
// → ถ้ารันในโหมดเก่า (sloppy) จะได้ "สวัสดี undefined" แทนทำไม? — function ใน setTimeout ถูกเรียกแบบ "function เปล่า ๆ" (กรณี 2) ไม่ใช่ method ของ user → this ไม่ใช่ user
5.4 Arrow Function แก้ปัญหา this
นี่คือความต่างที่สำคัญที่สุดของ arrow function:
Arrow function ไม่มี
thisของตัวเอง — มันใช้thisของ "ที่ที่มันถูกเขียน" (lexical this)
javascript
const user = {
name: "Alice",
greetLater() {
setTimeout(() => {
console.log("สวัสดี " + this.name); // ✅ ทำงานถูก!
}, 1000);
},
};
user.greetLater(); // "สวัสดี Alice"arrow function ใน setTimeout ไม่สร้าง this ใหม่ — มันใช้ this ของ greetLater (ซึ่งคือ user)
→ กฎ: ใน callback (setTimeout, event handler, array methods) — ใช้ arrow function จะได้ไม่มีปัญหา this
5.5 ⚠️ Arrow function ไม่เหมาะเป็น method ของ object
ตรงข้ามกัน — อย่าใช้ arrow function เป็น method:
javascript
const user = {
name: "Alice",
greet: () => {
return "สวัสดี " + this.name;
// ❌ this ไม่ใช่ user! — arrow ใช้ this ของ scope ข้างนอก object
// ใน module mode → this เป็น undefined → throw TypeError
},
};
user.greet();
// → TypeError (ใน module/strict mode)
// → ในโหมดเก่า (sloppy) ที่ this เป็น global object → "สวัสดี undefined"เพราะ arrow function ใช้ this ของ "ที่ที่เขียน" — ที่นี่คือ scope ข้างนอก object ไม่ใช่ตัว object
→ method ของ object — ใช้ function ปกติ (method shorthand) / callback — ใช้ arrow
5.6 สรุปความต่าง Arrow vs Function ปกติ
| Function ปกติ | Arrow Function | |
|---|---|---|
มี this ของตัวเอง | มี (ขึ้นกับวิธีเรียก) | ไม่มี (ใช้ของข้างนอก) |
มี arguments | มี | ไม่มี |
ใช้เป็น constructor (new) | ได้ | ไม่ได้ |
| เหมาะกับ | method, constructor | callback, function สั้น ๆ |
Part 6: bind, call, apply — ควบคุม this
3 method นี้ใช้ "กำหนด this เอง"
6.1 call — เรียก function พร้อมกำหนด this
call เรียกฟังก์ชันทันทีพร้อม "กำหนดว่า this คืออะไร" เป็นอาร์กิวเมนต์แรก ตามด้วยอาร์กิวเมนต์ปกติ — ใช้เมื่อต้องการยืมฟังก์ชันไปทำงานกับ object อื่น:
javascript
function introduce(greeting) {
return greeting + " ผมชื่อ " + this.name;
}
const alice = { name: "Alice" };
introduce.call(alice, "สวัสดี"); // "สวัสดี ผมชื่อ Alice"call(thisValue, arg1, arg2, ...) — เรียก function ทันที โดยกำหนด this
6.2 apply — เหมือน call แต่ส่ง argument เป็น array
javascript
introduce.apply(alice, ["สวัสดี"]); // เหมือน call แต่ argument อยู่ใน arrayปัจจุบันใช้น้อย — ใช้ call แทนได้โดยตรง: introduce.call(alice, "สวัสดี") หรือถ้ามี argument เป็น array อยู่แล้ว ใช้ spread: const args = ["สวัสดี"]; introduce.call(alice, ...args)
6.3 bind — สร้าง function ใหม่ที่ this ถูกล็อค
bind ไม่เรียก function ทันที — มันสร้าง "function ใหม่" ที่ this ถูกกำหนดไว้ตายตัว:
javascript
const aliceIntroduce = introduce.bind(alice);
aliceIntroduce("สวัสดี"); // "สวัสดี ผมชื่อ Alice"
aliceIntroduce("หวัดดี"); // "หวัดดี ผมชื่อ Alice"ใช้ตอนต้องการ "เก็บ function ที่ this ถูกล็อคไว้ใช้ทีหลัง"
Part 7: Higher-Order Function (ฟังก์ชันที่จัดการฟังก์ชัน)
Higher-Order Function = function ที่รับ function เป็น argument หรือ คืน function
7.1 รับ function เป็น argument
รูปแบบแรกของ higher-order function คือ "รับ function เข้ามาเป็น argument" แล้วเรียกใช้ภายใน — เป็นหัวใจของ callback และ method อย่าง map/filter/forEach:
javascript
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i); // เรียก function ที่ส่งเข้ามา
}
}
repeat(3, (i) => console.log("รอบ", i));
// รอบ 0
// รอบ 1
// รอบ 2array methods อย่าง forEach, map, filter ก็คือ higher-order function — มันรับ function เข้าไป
⚠️ กับดักที่ต้องรู้ล่วงหน้า:
forEachไม่รอ async function — ถ้าใส่async () => await ...ในforEachลูปจะ "ยิงคำสั่งทิ้งไว้" ทีเดียวพร้อมกันทั้งหมดแล้วผ่านไปต่อโดยไม่รอผล ทำให้ลำดับเพี้ยนและ error หลุดจับยาก งานที่ต้องรอผลทีละตัวให้ใช้for...ofกับawait(เรียนเต็มในบทที่ 09 — Async/Await)
7.2 คืน function
javascript
function withLogging(fn) {
return function(...args) {
console.log("เรียก function ด้วย:", args);
const result = fn(...args);
console.log("ได้ผลลัพธ์:", result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// เรียก function ด้วย: [2, 3]
// ได้ผลลัพธ์: 5withLogging ห่อ function เดิมด้วยความสามารถใหม่ (logging) — pattern นี้ทรงพลังมาก
Part 8: Pure Function — function ที่ "บริสุทธิ์"
Pure function = function ที่:
- input เดียวกัน → output เดียวกันเสมอ
- ไม่มี "ผลข้างเคียง" (side effect — ไม่ไปแก้อะไรข้างนอกตัวเอง เช่น แก้ตัวแปรนอก function, เขียนไฟล์, แสดงผลทางหน้าจอ, เรียก API)
javascript
// ✅ pure — input เดียวกันได้ output เดียวกัน ไม่แตะอะไรข้างนอก
function add(a, b) {
return a + b;
}
// ❌ impure — แก้ตัวแปรข้างนอก (side effect)
let total = 0;
function addToTotal(n) {
total += n; // ไปแก้ total ข้างนอก
}
// ❌ impure — ผลลัพธ์ไม่แน่นอน
function randomGreeting() {
return Math.random() > 0.5 ? "Hi" : "Hello";
}ทำไม pure function ดี:
- ทดสอบง่าย — input เดียวกัน output เดียวกันเสมอ
- คาดเดาได้ — ไม่มีผลข้างเคียงแอบแฝง
- debug ง่าย
→ พยายามเขียน pure function ให้มากที่สุด — ส่วนที่ต้องมี side effect (เขียนไฟล์, แสดงผล) ก็ทำ แต่แยกออกมาให้ชัด
Part 9: Recursion — function เรียกตัวเอง
Recursion = function ที่เรียกตัวเอง
javascript
function factorial(n) {
if (n <= 1) return 1; // กรณีฐาน (base case) — เงื่อนไขที่ทำให้หยุด
return n * factorial(n - 1); // เรียกตัวเอง
}
factorial(5); // 5 * 4 * 3 * 2 * 1 = 120ทุก recursion ต้องมี กรณีฐาน (base case = เงื่อนไขที่ทำให้หยุด) — ไม่งั้นจะเรียกตัวเองไม่จบ → โปรแกรม crash
javascript
// ❌ ไม่มี base case — เรียกตัวเองไม่จบ → "Maximum call stack size exceeded"
// (โปรแกรม crash: ขนาด call stack เกินขีดจำกัด — call stack คือ "กองซ้อน"
// ที่ JS ใช้จำว่ากำลังอยู่ใน function ไหน เรียกซ้อนลึกเกินก็ล้น)
function bad(n) {
return bad(n - 1);
}ส่วนใหญ่งานที่ใช้ recursion เขียนเป็น loop ก็ได้ — เลือกอันที่อ่านง่ายกว่า recursion เหมาะกับโครงสร้างที่ "ซ้อนกันเป็นชั้น" เช่น tree, โฟลเดอร์ในโฟลเดอร์
💡 ข้อจำกัดของ recursion ใน JavaScript: call stack มีขีดจำกัดประมาณ ~10,000 ชั้นใน V8 (Node.js/Chrome) — ถ้า recursion ลึกมากอาจ crash ด้วย "Maximum call stack size exceeded" ควรเปลี่ยนเป็น loop สำหรับข้อมูลที่มีความลึกมาก (ตัวเลข 10,000 เป็นแค่ตัวอย่างคร่าว ๆ ไม่ใช่ค่าคงที่ตายตัว — ขึ้นกับขนาด stack frame ของแต่ละฟังก์ชันและแพลตฟอร์มที่รัน)
Part 10: Lab — ลงมือทำ
Lab 1: Closure — Counter พร้อม reset
ฝึกใช้ closure สร้างตัวนับที่ "จำ" ค่า count ไว้ภายในโดยไม่เปิดให้ภายนอกแก้ตรง ๆ — เห็นพลังของ closure ในการซ่อน state (เป็นพื้นฐานของ encapsulation ใน JS):
javascript
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
reset: () => { count = 0; }, // ใช้ { } เพราะ count = 0 เป็น statement
// ไม่ใช่ expression ที่จะ return ค่า
getValue: () => count,
};
}
const c = makeCounter();
c.increment();
c.increment();
c.increment();
console.log(c.getValue()); // 3
c.reset();
console.log(c.getValue()); // 0Lab 2: Debounce — เทคนิคที่ใช้จริงในเว็บ
debounce = "รอให้หยุดทำสักพักก่อนค่อยทำงาน" — ใช้กับช่องค้นหา (รอผู้ใช้พิมพ์เสร็จก่อนค่อยค้นหา):
javascript
function debounce(fn, delay) {
let timeoutId; // เก็บใน closure
return function(...args) {
clearTimeout(timeoutId); // ยกเลิกอันเก่า
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
};
}
const search = debounce((keyword) => {
console.log("ค้นหา:", keyword);
}, 300);
// เรียกถี่ ๆ — แต่จะทำงานแค่ครั้งสุดท้าย หลังหยุด 300ms
search("a");
search("ap");
search("app");
// → 300ms หลังเรียกครั้งสุดท้าย: "ค้นหา: app"Lab 3: this — เปรียบเทียบ function ปกติ vs arrow
💡
setInterval(fn, ms)= "เรียกfnซ้ำทุกmsมิลลิวินาที" — ใช้สำหรับงานที่ต้องทำเป็นจังหวะ เช่น นับเวลา, อัปเดตหน้าจอ (หยุดด้วยclearInterval)⚠️ ระวัง:
setIntervalสร้าง timer ที่รันตลอด — ต้องเก็บ return value ไว้และเรียกclearIntervalเพื่อหยุด ไม่งั้นจะเป็น memory leak:javascriptconst id = setInterval(() => { ... }, 1000); clearInterval(id); // หยุด timer
javascript
const timer = {
seconds: 0,
startWrong() {
setInterval(function() {
this.seconds++; // ❌ this ผิด (function ปกติ — ใน module/strict mode
// this เป็น undefined → throw TypeError)
console.log(this.seconds); // ไม่ถึงบรรทัดนี้ — error ก่อน
}, 1000);
},
startRight() {
setInterval(() => {
this.seconds++; // ✅ this ถูก (arrow ใช้ this ของ timer)
console.log(this.seconds); // 1, 2, 3, ...
}, 1000);
},
};Part 11: Checkpoint
- "function เป็น first-class citizen" หมายความว่ายังไง?
- Function declaration ต่าง function expression ยังไง (เรื่อง hoisting)?
- Closure คืออะไร? อธิบายด้วยคำของคุณเอง
- Closure ใช้สร้าง "ตัวแปรส่วนตัว" ยังไง?
- ทำไม
varใน loop + closure ให้ผลผิด?letแก้ยังไง? thisคือ "ใครเรียก" หรือ "function อยู่ที่ไหน"?- ทำไม
thisหายใน callback ของsetTimeout? - Arrow function แก้ปัญหา
thisยังไง? - ทำไมไม่ควรใช้ arrow function เป็น method ของ object?
- Pure function คืออะไร? ทำไมถึงดี?
Part 12: สรุปบทนี้
- Function เป็น first-class citizen — เก็บใส่ตัวแปร, ส่งเป็น argument, คืนเป็นค่า ได้
- 5 วิธีสร้าง: declaration (hoisted), expression, arrow, method, IIFE
- Closure = function จำตัวแปรในสภาพแวดล้อมที่มันถูกสร้างได้ — ใช้สร้าง private state, memoization
- ใช้
let/constใน loop เพื่อหลีกเลี่ยงกับดัก closure this= "ใครเรียก function" ไม่ใช่ "function อยู่ที่ไหน"- Arrow function ไม่มี
thisของตัวเอง → ใช้ใน callback (ดี), อย่าใช้เป็น method (แย่) - bind/call/apply = ควบคุม
this - Higher-order function = function ที่จัดการ function อื่น
- Pure function = input เดียว output เดียว ไม่มี side effect — เขียนให้มากที่สุด
บทต่อไป — Objects และ Class
← บทที่ 04 | สารบัญ | บทที่ 06: Objects →
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-12