Skip to content

บทที่ 05 — Function เชิงลึก: Closure และ this

← บทที่ 04 | สารบัญ | บทที่ 06: Objects →

บทนี้คือ บทที่สำคัญที่สุดของ JavaScript — เพราะ JavaScript เป็น "ภาษาที่ function เป็นพระเอก"

มี 3 หัวข้อที่ทุกคนต้องเข้าใจให้ลึก — และเป็น 3 หัวข้อที่ถูกถามในการสัมภาษณ์งานบ่อยที่สุด:

  1. Closure — function ที่ "จำ" สภาพแวดล้อมของมันได้
  2. this — ตัวแปรพิเศษที่เปลี่ยนความหมายตามวิธีเรียก
  3. 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);    // 5

2.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 = ใช้ default

3.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

เกิดอะไรขึ้น — ลองคิดดี ๆ:

  1. เรียก makeCounter() — มันสร้างตัวแปร count = 0 แล้ว return function ข้างในออกมา
  2. หลัง makeCounter() จบ — ปกติ count ควรหายไป (เพราะ function จบแล้ว)
  3. แต่มันไม่หาย — เพราะ function ข้างในที่ return ออกมา "ยังจำ count ได้" → count เลยมีชีวิตอยู่ต่อ
  4. ทุกครั้งที่เรียก 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);       // 15

double คือ 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, 2

let สร้าง 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 object

global 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, constructorcallback, 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
// รอบ 2

array 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]
// ได้ผลลัพธ์: 5

withLogging ห่อ function เดิมด้วยความสามารถใหม่ (logging) — pattern นี้ทรงพลังมาก


Part 8: Pure Function — function ที่ "บริสุทธิ์"

Pure function = function ที่:

  1. input เดียวกัน → output เดียวกันเสมอ
  2. ไม่มี "ผลข้างเคียง" (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());     // 0

Lab 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:

javascript
const 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

  1. "function เป็น first-class citizen" หมายความว่ายังไง?
  2. Function declaration ต่าง function expression ยังไง (เรื่อง hoisting)?
  3. Closure คืออะไร? อธิบายด้วยคำของคุณเอง
  4. Closure ใช้สร้าง "ตัวแปรส่วนตัว" ยังไง?
  5. ทำไม var ใน loop + closure ให้ผลผิด? let แก้ยังไง?
  6. this คือ "ใครเรียก" หรือ "function อยู่ที่ไหน"?
  7. ทำไม this หายใน callback ของ setTimeout?
  8. Arrow function แก้ปัญหา this ยังไง?
  9. ทำไมไม่ควรใช้ arrow function เป็น method ของ object?
  10. 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