JavaScript
Class
Class Expression
เข้าใจการสร้าง class ในรูปแบบ expression — เก็บ class ไว้ในตัวแปร — ใช้ anonymous และ named — เลือก class ตามเงื่อนไข — และส่ง class เป็น argument — ฝึกครบทั้ง 5 lab ตั้งแต่ anonymous, named, getter/method/private, เงื่อนไข ไปจนถึง inheritance
Class Expression คืออะไร — เก็บ class ไว้ในตัวแปร
**Class Expression** คือการสร้าง class ในรูปแบบ expression — แล้วนำไปเก็บไว้ในตัวแปร — เช่นเดียวกับที่ `const fn = function() { }` เก็บฟังก์ชันในตัวแปร `const MyClass = class { ... }` — ด้านขวาของ `=` คือ class expression — มัน evaluate แล้วได้ class — ตัวแปร `MyClass` จะเก็บ class นั้นไว้ Class Expression มี 2 รูปแบบ: - **Anonymous** — `const MyClass = class { ... }` — class ไม่มีชื่อด้านใน - **Named** — `const MyClass = class ClassName { ... }` — class มีชื่อด้านใน `ClassName` **เทียบกับ Class Declaration (`class MyClass { }`)**: Declaration ประกาศ class ใน scope เลย — ส่วน Expression สร้าง class แล้ว assign ให้ตัวแปร — ทั้งสองแบบใช้ `new` สร้าง instance ได้เหมือนกัน
`const User = class { }` — ตัวแปร `User` เก็บ class — ใช้ `new User()` สร้าง instance
// Anonymous Class Expression
const User = class {
constructor(name) {
this.name = name;
}
greet() {
return "สวัสดี " + this.name;
}
};
const u = new User("สมชาย");
console.log(u.greet()); // "สวัสดี สมชาย"
console.log(u instanceof User); // trueจากตัวอย่าง: - `const User = class { ... }` — class expression แบบ anonymous — ตัวแปร `User` ชี้ไปที่ class นี้ - `new User('สมชาย')` — สร้าง instance — syntax เหมือน class declaration ทุกประการ - `u instanceof User` — ได้ `true` — `User` เป็น class จริง - class expression ให้ผลลัพธ์เป็น class — เราเอาไปทำอะไรกับ class ปกติได้ทุกอย่าง **วิธีคิด**: Class Declaration คือ "ประกาศชื่อ class" — Class Expression คือ "คำนวณ class ออกมาแล้วตั้งชื่อให้ตัวแปร" — ผลลัพธ์สุดท้ายใช้ `new` ได้เหมือนกัน
Class Expression ไม่ถูก hoist — ต้องประกาศก่อนใช้
Class Declaration มี hoisting แบบหนึ่ง — คุณใช้ class หลังบรรทัดประกาศได้ แต่ถ้าอยู่ใน Temporal Dead Zone (TDZ) ก่อนถึงบรรทัด `class` จะเข้าไม่ถึง Class Expression ใช้ `const` เก็บ — มันจึงทำตามกฎของ `const` — ตัวแปรจะยังไม่มีค่าจนกว่าบรรทัด `const MyClass = class { }` จะทำงาน — ถ้าเรียกก่อนจะได้ `ReferenceError`
`new Product()` ก่อนถึงบรรทัด `const` → `ReferenceError: Cannot access 'Product' before initialization`
// ❌ เรียกก่อนประกาศ — error
try {
const p = new Product("ปากกา");
} catch (e) {
console.log(e.name + ": " + e.message);
// ReferenceError: Cannot access 'Product' before initialization
}
const Product = class {
constructor(name) {
this.name = name;
}
};
// ✅ เรียกหลังประกาศ — ทำงานได้
const p2 = new Product("สมุด");
console.log(p2.name); // "สมุด"| เรื่องที่เทียบ | Class Declaration | Class Expression |
|---|---|---|
| syntax | `class User { }` | `const User = class { }` |
| เรียกก่อนประกาศ | ได้ (ถ้าไม่ติด TDZ) | ไม่ได้ — ต้องรอ `const` ทำงานก่อน |
| hoisting | ชื่อ class ถูก hoist — แต่อยู่ใน TDZ | ไม่ hoist — ตัวแปรตามกฎ `const` |
| สิ่งที่เราใช้ `new` ด้วย | ชื่อ class โดยตรง | ชื่อตัวแปร |
| ชื่อใน stack trace | ชื่อ class (เช่น `User`) | anonymous → `<anonymous>` / named → ชื่อด้านใน |
**ข้อสังเกต**: Function Expression ก็มีพฤติกรรมเดียวกัน — `const fn = function() { }` ต้องประกาศก่อนเรียกเช่นกัน — Class Expression ทำตาม pattern เดียวกันเป๊ะ
Named Class Expression — ชื่อภายในมองเห็นเฉพาะใน body
`const MyClass = class ClassName { ... }` — `ClassName` คือชื่อภายใน — มันมองเห็นได้เฉพาะใน body ของ class นี้เท่านั้น **ประโยชน์ของชื่อภายใน**: - **self-reference** — ถ้าต้องการอ้างถึง class ตัวเองจาก static method — ใช้ชื่อภายในแทน `this` ได้ - **error / debug** — ชื่อภายในจะปรากฏใน stack trace, `instanceof` check, และ error message — ทำให้ debug ง่ายกว่า anonymous class **ข้อควรรู้**: ชื่อภายใน `ClassName` **ไม่ใช่ตัวแปรนอก class** — คุณใช้ `new ClassName()` จากภายนอกไม่ได้ — ต้องใช้ `new MyClass()` ผ่านชื่อตัวแปร
`const Person = class PersonDetail { }` — ใน body ใช้ `PersonDetail` อ้างตัวเอง — นอก body ใช้ `Person`
const Person = class PersonDetail {
constructor(name, age) {
this.name = name;
this.age = age;
}
static create(name, age) {
// self-reference — ใช้ชื่อภายใน PersonDetail
return new PersonDetail(name, age);
}
get info() {
// ดูชื่อ class ผ่าน this.constructor.name
return "class: " + this.constructor.name;
}
};
const p = Person.create("สมชาย", 25);
console.log(p.name); // "สมชาย"
console.log(p.age); // 25
console.log(p.info); // "class: PersonDetail"
// ภายนอกเข้าถึงชื่อภายในไม่ได้
// console.log(PersonDetail); // ❌ ReferenceError
// console.log(new PersonDetail()); // ❌ ReferenceError
console.log(p instanceof Person); // trueจากตัวอย่าง: - `const Person = class PersonDetail { ... }` — `PersonDetail` คือชื่อภายใน — `Person` คือชื่อตัวแปรภายนอก - `static create(...)` — static method ใช้ `new PersonDetail(...)` — self-reference ผ่านชื่อภายใน - `this.constructor.name` — ได้ `"PersonDetail"` — ไม่ใช่ `"Person"` - ภายนอกใช้ `Person.create(...)` เท่านั้น — `PersonDetail` เป็นชื่อที่มองไม่เห็นจากภายนอก **เมื่อไรควรใช้ named**: - ตอน debug — ชื่อใน stack trace จะช่วยให้ตามหา class ถูกตัว - ตอนต้องการ self-reference ใน static method — ใช้ชื่อภายในตรง ๆ - ถ้าไม่จำเป็นต้อง debug หรือ self-reference — anonymous ก็เพียงพอ
ใช้ Class Expression กับ feature ทั้งหมดของ class
Class Expression รองรับทุก feature ที่คุณเรียนมา — constructor, methods, getter/setter, static fields, private fields, และ inheritance (`extends`) ยกตัวอย่าง class expression หนึ่งตัวที่ใช้ทุก feature ร่วมกัน:
`const BankAccount = class { ... }` — มี `#balance` (private), `get balance()` (getter), `set balance()` (setter + validation), method `deposit()`, static `bankName`
const BankAccount = class {
#balance = 0; // private field
static bankName = "Siam Bank"; // static
constructor(owner, initialBalance) {
this.owner = owner;
this.balance = initialBalance; // เรียก setter — validation
}
get balance() {
return this.#balance;
}
set balance(value) {
if (value < 0) {
console.log("ยอดเงินต้องไม่ต่ำกว่า 0");
return;
}
this.#balance = value;
}
deposit(amount) {
if (amount <= 0) {
return "จำนวนฝากต้องมากกว่า 0";
}
this.#balance = this.#balance + amount;
return "ฝาก " + amount + " สำเร็จ — ยอดคงเหลือ " + this.#balance;
}
};
const acc = new BankAccount("สมชาย", 1000);
console.log(acc.owner); // "สมชาย"
console.log(acc.balance); // 1000 — getter
acc.balance = 2000; // setter — validation
console.log(acc.balance); // 2000
console.log(acc.deposit(500)); // "ฝาก 500 สำเร็จ — ยอดคงเหลือ 2500"
console.log(BankAccount.bankName); // "Siam Bank" — static
console.log(acc instanceof BankAccount); // trueตัวอย่างนี้ครบทุกเรื่อง: - `#balance` — private field — ภายนอกเข้าไม่ถึงตรง ๆ - `get balance()` — getter — `acc.balance` อ่านได้ - `set balance(value)` — setter + validation — กันยอดติดลบ - `deposit(amount)` — method ปกติ — รับ parameter, มี logic `if`, `return` - `static bankName` — static field — เข้าถึงผ่านชื่อ class โดยตรง **ข้อสังเกต**: ตัวอย่างนี้ทำงานได้ — ทุก feature ที่คุณรู้จักใช้กับ Class Expression ได้เหมือน Class Declaration ทุกประการ
เลือก class ตามเงื่อนไข — จุดแข็งของ Class Expression
เพราะ class expression ให้ผลลัพธ์เป็น class — เราสามารถ **เลือก** ว่าจะ assign class ไหนให้ตัวแปร — ตามเงื่อนไข ณ runtime นี่คือ pattern ที่ Class Declaration ทำไม่ได้ — เพราะ Class Declaration ผูกชื่อกับ class ตายตัวตั้งแต่ compile-time — Class Expression ให้น้ำหนักเราตอน runtime — เลือก class ได้ตาม logic ตัวอย่างง่ายสุดคือใช้ `if/else` เลือก class:
ตัวแปร `UserClass` จะได้ class คนละตัว — ขึ้นอยู่กับ `isAdmin` — แล้วสร้าง instance จาก class ที่เลือก
const isAdmin = true;
let UserClass;
if (isAdmin) {
UserClass = class {
constructor(name) {
this.name = name;
this.role = "admin";
}
};
} else {
UserClass = class {
constructor(name) {
this.name = name;
this.role = "member";
}
};
}
const user = new UserClass("สมชาย");
console.log(user.name); // "สมชาย"
console.log(user.role); // "admin" — เพราะ isAdmin = trueจากตัวอย่าง: - `let UserClass` — ประกาศตัวแปรไว้ก่อน — ยังไม่มีค่า - `if (isAdmin)` — เช็กเงื่อนไข — `UserClass` จะได้ class คนละตัว - `new UserClass(...)` — สร้าง instance จาก class ที่ถูกเลือก **Pattern นี้คือ Factory** — เลือก class ตาม input — สร้าง instance จาก class ที่เหมาะสม — โค้ดที่เหลือใช้ instance นั้นได้โดยไม่ต้องรู้ว่าเลือก class ไหนมาตอนแรก **เทียบกับ Class Declaration**: ถ้าอยากทำ pattern นี้ด้วย Declaration — ต้องใช้ `switch` หรือ `if` แยกสร้าง instance ทีละเงื่อนไข — Expression แยก logic เลือก class ออกจาก logic สร้าง instance ได้ — โค้ดสะอาดกว่า
ฟังก์ชัน `createUser` รับ `isAdmin` — เลือก class — สร้าง instance — return instance กลับ
function createUser(name, isAdmin) {
const UserClass = isAdmin
? class {
constructor(name) {
this.name = name;
this.role = "admin";
}
}
: class {
constructor(name) {
this.name = name;
this.role = "member";
}
};
return new UserClass(name);
}
const u1 = createUser("สมชาย", true);
console.log(u1.role); // "admin"
const u2 = createUser("สมหญิง", false);
console.log(u2.role); // "member"ตัวอย่างนี้ใช้ ternary — เลือก class expression ใน `? :` — แล้ว `return new UserClass(name)` **ข้อดีของ pattern นี้**: - logic เลือก class อยู่ใกล้กัน — อ่านรู้เรื่องในที่เดียว - ข้างนอกใช้แค่ `createUser(...)` — ไม่ต้องรู้ว่ามีกี่ class - เพิ่ม class ใหม่ในอนาคต — แค่เพิ่ม branch — โค้ดที่เรียกใช้ไม่เปลี่ยน
ส่ง class เป็น argument — class as first-class value
Class Expression ตอกย้ำแนวคิดว่า **class เป็น value** — คุณ assign ให้ตัวแปรได้ — และคุณส่ง class ไปเป็น argument ให้ฟังก์ชันอื่นได้ — ฟังก์ชันนั้นสร้าง instance จาก class ที่ได้รับมา ลองดูตัวอย่าง: ฟังก์ชันที่รับ class แล้วสร้าง instance พร้อมเรียก method:
`spawn(clazz, ...args)` — รับ class เป็น parameter — สร้าง instance ด้วย `new clazz(...args)` — return instance
function spawn(clazz, ...args) {
return new clazz(...args);
}
const Dog = class {
constructor(name) {
this.name = name;
}
bark() {
return this.name + " โฮ่ง!";
}
};
const Cat = class {
constructor(name) {
this.name = name;
}
meow() {
return this.name + " เหมียว!";
}
};
const d = spawn(Dog, "ลัคกี้");
console.log(d.bark()); // "ลัคกี้ โฮ่ง!"
const c = spawn(Cat, "มิ้ว");
console.log(c.meow()); // "มิ้ว เหมียว!"จากตัวอย่าง: - `spawn(clazz, ...args)` — parameter `clazz` คือ class — ฟังก์ชันไม่รู้ว่าคือ class อะไร — แค่ใช้ `new` กับมัน - `spawn(Dog, 'ลัคกี้')` — ส่ง class `Dog` เข้าไป — ได้ instance ของ `Dog` - `spawn(Cat, 'มิ้ว')` — ส่ง class `Cat` เข้าไป — ได้ instance ของ `Cat` **นี่คือจุดที่ Class Expression โดดเด่นกว่า Declaration** — Expression เน้นว่า class คือ value ที่ส่งต่อได้ — Declaration เน้นประกาศชื่อตายตัว — ทั้งสองแบบใช้ `new` ได้ แต่ Expression เปิดทางให้ class เป็น first-class citizen — ส่งต่อ, รับเข้า, assign, return ได้หมด **Use-case จริง**: Dependency Injection, Plugin System, Strategy Pattern — คุณส่ง class ที่มี interface เดียวกันเข้าไปในระบบ — ระบบสร้าง instance และเรียก method — โดยไม่ต้องรู้ว่าเป็น class อะไร
กฎสำคัญของ Class Expression
| กฎ | คำอธิบาย | ตัวอย่าง |
|---|---|---|
| Class Expression evaluate แล้วได้ class | `const A = class { }` — ด้านขวาคือ expression — ผลลัพธ์คือ class — assign ให้ `A` | `const A = class { };` ✅ / `new A()` ✅ |
| ต้องประกาศก่อนใช้ | ตัวแปรที่เก็บ class expression ต้องประกาศก่อนเรียก `new` — เพราะเป็น `const` / `let` | `new A()` ก่อน `const A = class { }` → ❌ ReferenceError |
| Anonymous class — ชื่อใน `instanceof` มาจากตัวแปร | ถ้าใช้ `class { }` ไม่มีชื่อ — `instance.constructor.name` จะเป็น `''` (empty string) หรือชื่อตัวแปร (ขึ้นกับ engine) | `const A = class { }; new A().constructor.name` → `''` หรือ `'A'` |
| Named class — ชื่อภายในใช้ได้ใน body เท่านั้น | `const A = class B { }` — `B` มองเห็นใน body ของ class — ภายนอกใช้ `A` | `new B()` → ❌ ReferenceError — ภายนอกไม่รู้จัก `B` |
| Named class — ชื่อภายในเป็นค่าคงที่ใน body | ชื่อภายในถูก bind ไว้กับ class — ใน body คุณ assign ค่าใหม่ให้มันไม่ได้ — คล้าย `const` | `const A = class B { };` — `B = 5;` ใน body → ❌ |
| Class Expression ใช้ `extends` ได้ | `const Child = class extends Parent { }` — inheritance ทำงานเหมือน Declaration | `const Child = class extends Parent { };` ✅ |
| Class Expression ซ้อนใน expression อื่นได้ | ใช้ class expression ใน array, object, function argument, ternary, return ได้ — เพราะมันคือ expression | `[class { }, class { }]` ✅ / `fn(class { })` ✅ |
ข้อผิดพลาดที่พบบ่อย
- **เรียก `new` ก่อนประกาศ**: `new A()` ก่อน `const A = class { }` → `ReferenceError: Cannot access 'A' before initialization` — **แก้**: ประกาศ class expression ก่อนใช้ `new`
- **ใช้ชื่อภายในจากภายนอก**: `const A = class B { }; console.log(B);` → `ReferenceError: B is not defined` — ชื่อภายในใช้ได้แค่ใน body — ภายนอกใช้ชื่อตัวแปร `A`
- **สับสนระหว่าง anonymous กับ named**: `const A = class { }; console.log(A.name);` — อาจได้ `"A"` หรือ `""` ขึ้นอยู่กับ engine — ถ้าอยากให้ชื่อแน่นอน — ใช้ named `class B { }`
- **ลืมว่า class expression เป็น expression — ต้องมี `;` หลัง `}`**: `const A = class { }` — ควรมี `;` หลังปิด `}` — เหมือน `const x = 5;` — ถ้าลืม อาจเกิดปัญหาเวลามี IIFE หรือ `(` ตามหลังในบรรทัดถัดไป
- **เข้าใจผิดว่า Class Expression ใช้ `new` ไม่ได้**: หลายคนคิดว่า `const A = class { }` แล้ว `A` ไม่ใช่ class — แต่จริง ๆ `A` เป็น class — ใช้ `new A()` ได้ — `instanceof` ได้ — ทำได้ทุกอย่างเหมือน class ปกติ
- **ใช้ `let` ประกาศ class expression โดยไม่จำเป็น**: `let A = class { }` — มีโอกาสถูก assign ทับ — class ควรใช้ `const` เพื่อกันการเปลี่ยน — `let` ใช้เมื่อต้องการเลือก class ตามเงื่อนไขเท่านั้น
- **ลืม constructor ใน class expression**: `const A = class { }` ไม่มี constructor — `new A()` ได้ instance เปล่า — เหมือน class declaration เปล่า — ไม่ผิด แต่ควรรู้ว่าถ้าอยากรับ parameter — ต้องเขียน constructor