Types และ Coercion
Primitives vs objects
หัวข้อที่มีชื่อว่า “Primitives vs objects”JavaScript มีค่าสองประเภท:
Primitives — ไม่เปลี่ยนแปลงได้ (immutable), เก็บโดยค่า (by value):
string,number,bigint,boolean,undefined,null,symbol
Objects — เปลี่ยนแปลงได้ (mutable), เก็บโดยอ้างอิง (by reference):
- Plain objects
{}, arrays[], functions,Map,Set,Dateเป็นต้น
// Primitives: each variable holds an independent copylet a = 42;let b = a;b = 99;console.log(a); // 42 — unchanged
// Objects: both variables point to the same objectconst x = { count: 0 };const y = x;y.count = 5;console.log(x.count); // 5 — same objecttypeof คืนค่าสตริงที่อธิบาย type ของค่า ผลลัพธ์บางอย่างน่าแปลกใจ:
typeof 42 // 'number'typeof 'hello' // 'string'typeof true // 'boolean'typeof undefined // 'undefined'typeof null // 'object' ← historical bug, not fixabletypeof {} // 'object'typeof [] // 'object' ← arrays are objectstypeof function(){} // 'function'typeof Symbol() // 'symbol'typeof 9007n // 'bigint'ความผิดพลาด typeof null === 'object' มีอยู่ใน spec ตั้งแต่ JavaScript 1.0 หากต้องการตรวจสอบ null โดยเฉพาะ ใช้ value === null
== vs ===
หัวข้อที่มีชื่อว่า “== vs ===”== ทำ type coercion ก่อนเปรียบเทียบ ส่วน === ไม่ทำ — ตรวจสอบทั้งค่าและ type
0 == false // true — false coerces to 00 === false // false — number vs boolean
'' == false // true — both coerce to 0'' === false // false
null == undefined // true — special rulenull === undefined // false
1 == '1' // true — '1' coerces to 11 === '1' // falseTruthy และ Falsy
หัวข้อที่มีชื่อว่า “Truthy และ Falsy”ค่าทุกตัวเป็น truthy หรือ falsy เมื่อใช้ใน context ของ boolean
Falsy values — มีเพียงหกตัวเท่านั้น:
false,0,''(สตริงว่าง),null,undefined,NaN
ทุกอย่างอื่นเป็น truthy — รวมถึง [], {}, '0' และ 'false'
if ([]) console.log('empty array is truthy'); // printsif ({}) console.log('empty object is truthy'); // printsif (0) console.log('zero is truthy'); // does NOT printDemo Coercion ที่รันได้
หัวข้อที่มีชื่อว่า “Demo Coercion ที่รันได้”// Spot the coercion surprisesconsole.log(0 == false); // trueconsole.log(0 === false); // falseconsole.log(null == undefined); // trueconsole.log(null === undefined); // falseconsole.log([] == false); // true — [] coerces to '' coerces to 0console.log(Boolean([])); // true — [] is truthy!console.log(typeof null); // objectกับดัก coercion เพิ่มเติม
หัวข้อที่มีชื่อว่า “กับดัก coercion เพิ่มเติม”if, +, -, Number() และ parseInt() แต่ละตัว coerce ในแบบที่ต่างกันและจำสับสนได้ง่าย
if สนใจแค่ truthy/falsy — ไม่สนว่า “นี่คือค่าที่มีความหมายหรือเปล่า” 0, NaN และ '' เป็น falsy ทั้งหมดและตกอยู่ branch เดียวกับ null/undefined แม้ว่า 0 และ '' จะเป็นข้อมูลที่ถูกต้องได้:
if (0) console.log('never — 0 is falsy');if (NaN) console.log('never — NaN is falsy');if ('') console.log('never — empty string is falsy');if ([]) console.log('runs — an empty array is truthy');if ({}) console.log('runs — an empty object is truthy');+ และ - ไม่เห็นตรงกันเรื่องสตริง + ถูก overload ทั้งบวกเลขและต่อสตริง ดังนั้นถ้า operand ฝั่งใดฝั่งหนึ่งเป็นสตริง + จะต่อสตริง ส่วน - ไม่มีความหมายสำหรับสตริงเลย จึง coerce ทั้งสองฝั่งเป็นตัวเลขก่อนเสมอ:
console.log(1 + '1'); // '11' — + sees a string, concatenatesconsole.log(1 - '1'); // 0 — - always coerces to numberconsole.log([5] - 1); // 4 — [5] coerces to the number 5 firstconsole.log([] + []); // '' — both sides coerce to '', then concatenateNumber('') เป็น 0 ไม่ใช่ NaN สตริงว่างหรือมีแต่ whitespace จะแปลงเป็น 0 ด้วย Number() แต่ parseInt('') ให้ NaN — ทั้งสองฟังก์ชันไม่ตรงกันสำหรับ input ว่าง:
console.log(Number('')); // 0console.log(Number(' ')); // 0console.log(parseInt('')); // NaNparseInt ที่ไม่มี radix เป็นกับดักจริง — แต่ไม่ใช่ตัวที่คนพูดซ้ำๆ กัน เรื่อง “leading zero แปลว่า octal” แบบเก่าไม่จริงแล้ว (parseInt('08') ให้ 8 บน engine สมัยใหม่ทุกตัว ตาม spec ES5+) กับดักจริงในปัจจุบันคือสตริงที่ขึ้นต้นด้วย 0x ที่ auto-detect เป็น hex และการเรียกโดยไม่ระบุ radix ชัดเจนที่เชื่อพฤติกรรมนั้นเงียบๆ:
console.log(parseInt('08')); // 8 — no octal trap on modern JS enginesconsole.log(parseInt('0x10')); // 16 — auto-detected as hex, radix defaulted to 16console.log(parseInt('10', 2)); // 2 — radix 2: '10' means binary twoconsole.log(parseInt('19,000')); // 19 — stops at the first non-digit characterระบุ radix ชัดเจนเสมอ — parseInt(str, 10) — เมื่อต้องการฐาน 10
Array.isArray() คือการตรวจ array ที่เชื่อถือได้ตัวเดียว typeof รายงาน 'object' สำหรับ array (ดูด้านบน) และ [] instanceof Array ก็ fail ข้าม realm ต่างกัน (iframe, worker thread, vm context):
console.log(Array.isArray([])); // trueconsole.log(Array.isArray({})); // falseconsole.log(typeof []); // 'object' — no help hereOptional chaining และ nullish coalescing เทียบกับ ||
หัวข้อที่มีชื่อว่า “Optional chaining และ nullish coalescing เทียบกับ ||”?. short-circuit เป็น undefined แทนที่จะ throw เมื่อลิงก์ใน chain ของ property/method เป็น null หรือ undefined ?? เลือกค่าฝั่งขวาเฉพาะเมื่อฝั่งซ้ายเป็น null หรือ undefined เท่านั้น — ต่างจาก || ที่ fall through สำหรับ falsy value ทุกตัว รวมถึง 0 และ '':
const config = { retries: 0, name: '' };
console.log(config.retries || 3); // 3 — WRONG: 0 is a valid retry count, but || treats it as missingconsole.log(config.retries ?? 3); // 0 — RIGHT: ?? only falls back on null/undefined
console.log(config.timeout ?? 5000); // 5000 — timeout is genuinely absentconsole.log(config.user?.profile?.email); // undefined — no throw, even though user is missingflowchart TD
A["left ?? right"] --> B{"Is left null or undefined?"}
B -->|"yes"| C["use right"]
B -->|"no, even if 0, '', false, NaN"| D["use left"]
E["left || right"] --> F{"Is left falsy? (0, '', false, NaN, null, undefined)"}
F -->|"yes"| C
F -->|"no"| D การ Clone object: structuredClone vs spread
หัวข้อที่มีชื่อว่า “การ Clone object: structuredClone vs spread”การ spread object ({ ...obj }) หรือ array ([...arr]) copy แค่ ระดับบนสุด เท่านั้น — nested object ยังคงถูกแชร์โดยการอ้างอิง structuredClone() ทำ deep clone ที่แท้จริง (และสร้าง Date, Map, Set, typed array ใหม่อย่างถูกต้อง) แต่ clone function ไม่ได้:
const original = { name: 'demo', tags: ['a', 'b'], meta: { count: 1 } };
const shallow = { ...original };shallow.meta.count = 99;console.log(original.meta.count); // 99 — shallow copy shared the nested "meta" object
const deep = structuredClone(original);deep.meta.count = 1;console.log(original.meta.count); // still 99 — deep clone is fully independentJSON.parse(JSON.stringify(obj)) เป็นเทคนิค deep-clone ad hoc ที่ใช้กันบ่อย แต่มันทิ้งค่า undefined และ function ไปเงียบๆ และเปลี่ยน Date object เป็นสตริงธรรมดา — structuredClone() จัดการสิ่งเหล่านี้ได้ถูกต้องทั้งหมดและควรเลือกใช้แทน
Demo coercion และ cloning ที่รันได้
หัวข้อที่มีชื่อว่า “Demo coercion และ cloning ที่รันได้”Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”operator ที่ coerce แต่ละตัวตอบคำถามที่ต่างกันเล็กน้อย: if ถาม “นี่ truthy ไหม?”, + ถาม “ฝั่งใดฝั่งหนึ่งดูเหมือนสตริงไหม?”, -/Number() ถาม “นี่คือเลขอะไร?” และ ?? ถาม “นี่คือ null หรือ undefined จริงๆ ไหม?” ไม่มีตัวไหนถาม “ผู้เรียกให้ค่าที่มีความหมายมาจริงไหม?” — คำถามนั้นเป็นคำถามที่ 0, '' และ NaN ตอบให้คุณไม่ได้เลย เพราะมันเป็นค่าที่ถูกต้องแต่บังเอิญเป็น falsy การอ่านโค้ดที่มี coercion เยอะๆ ให้เข้าใจดี หมายถึงการลอง substitute 0, '', [] และ {} เข้าไปในใจก่อนที่จะเชื่อ truthy check ทุกตัว
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
if (value) / value || fallback | สั้น อ่านง่ายสำหรับ “นี่หายไปไหม” | บอกไม่ได้ว่า “หายไป” ต่างจาก 0, '' หรือ false จริงๆ |
value ?? fallback | ถือว่า null/undefined เท่านั้นที่หายไป | คุ้นเคยน้อยกว่าสำหรับนักพัฒนาที่เคยชินกับ || อย่างเดียว |
{ ...obj } spread | เร็ว ไม่มี dependency เหมาะกับ object แบน | nested object/array ถูกแชร์โดยการอ้างอิง — mutate clone อาจ mutate ต้นฉบับ |
structuredClone(obj) | deep clone จริง จัดการ Date/Map/Set ได้ถูกต้อง | clone function หรือ DOM node ไม่ได้; ช้ากว่า shallow spread เล็กน้อย |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
0และ''มีความหมายเหมือนnull/undefinedในif” ทั้งหมดเป็น falsy แต่0และ''เป็นค่าที่มีอยู่จริง —??(ไม่ใช่||ไม่ใช่if (!value)) คือ operator ที่แยก “หายไป” ออกจาก “falsy แต่มีอยู่” ได้จริง - “
parseInt('08')ให้คำตอบผิดเพราะ octal parsing” เป็นจริงใน engine ก่อน ES5 บน engine ทุกตัวที่คอร์สนี้ยึดparseInt('08') === 8กับดักparseIntจริงในวันนี้คือการไม่ระบุ radix บน input ที่กำกวมหรือขึ้นต้นด้วย0x - “การ spread object (
{ ...obj }) สร้าง copy ที่เป็นอิสระเต็มที่” มัน copy แค่ระดับเดียวเท่านั้น — nested object หรือ array ยังเป็น reference เดิมในทั้งสอง copy - “
typeof arr === 'object'ใช้ตรวจ array ได้” มันเป็น true ทั้งกับ plain object และ array จึงแยกไม่ได้ — ใช้Array.isArray()แทน
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”รูปแบบที่พบบ่อยมากในโค้ดที่ AI สร้างคือ truthy check ที่ใช้แทนการเช็ค “ค่านี้ถูกส่งมาไหม” — if (user.count), value || 'default' หรือ data.length && doSomething() — ไม่มีตัวไหนทำงานถูกต้องเลยเมื่อค่าจริงเป็น 0, '' หรือ false เมื่อไหร่ก็ตามที่ค่า falsy-but-valid เป็นไปได้ การเช็คต้องเป็น !== undefined, !== null หรือ ??
🐛 หา bug ในโค้ดนี้
function renderBadge(user) {if (user.count) { return user.count + ' items';}return 'No items yet';}
console.log(renderBadge({ count: 0 })); // meant to show "0 items"เฉลย
if (user.count) มองว่า 0 เป็น falsy ดังนั้น user ที่มี count เป็นศูนย์พอดีจะตกไปที่ branch “No items yet” — แม้ 0 จะเป็น count ที่ถูกต้องและรู้ค่าแล้ว ไม่ใช่ค่าที่หายไป วิธีแก้คือเช็คการ ไม่มี ค่านั้นโดยตรง: if (user.count !== undefined) หรือปรับโครงสร้างด้วย nullish coalescing เพื่อให้ undefined/null เท่านั้นที่ fallback ไปค่า default ในขณะที่ 0 จริงยังพิมพ์เป็น “0 items”