ข้ามไปยังเนื้อหา

Types และ Coercion

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 copy
let a = 42;
let b = a;
b = 99;
console.log(a); // 42 — unchanged
// Objects: both variables point to the same object
const x = { count: 0 };
const y = x;
y.count = 5;
console.log(x.count); // 5 — same object

typeof คืนค่าสตริงที่อธิบาย type ของค่า ผลลัพธ์บางอย่างน่าแปลกใจ:

typeof 42 // 'number'
typeof 'hello' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object' ← historical bug, not fixable
typeof {} // 'object'
typeof [] // 'object' ← arrays are objects
typeof function(){} // 'function'
typeof Symbol() // 'symbol'
typeof 9007n // 'bigint'

ความผิดพลาด typeof null === 'object' มีอยู่ใน spec ตั้งแต่ JavaScript 1.0 หากต้องการตรวจสอบ null โดยเฉพาะ ใช้ value === null

== ทำ type coercion ก่อนเปรียบเทียบ ส่วน === ไม่ทำ — ตรวจสอบทั้งค่าและ type

0 == false // true — false coerces to 0
0 === false // false — number vs boolean
'' == false // true — both coerce to 0
'' === false // false
null == undefined // true — special rule
null === undefined // false
1 == '1' // true — '1' coerces to 1
1 === '1' // false

ค่าทุกตัวเป็น truthy หรือ falsy เมื่อใช้ใน context ของ boolean

Falsy values — มีเพียงหกตัวเท่านั้น:

  • false, 0, '' (สตริงว่าง), null, undefined, NaN

ทุกอย่างอื่นเป็น truthy — รวมถึง [], {}, '0' และ 'false'

if ([]) console.log('empty array is truthy'); // prints
if ({}) console.log('empty object is truthy'); // prints
if (0) console.log('zero is truthy'); // does NOT print
// Spot the coercion surprises
console.log(0 == false); // true
console.log(0 === false); // false
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log([] == false); // true — [] coerces to '' coerces to 0
console.log(Boolean([])); // true — [] is truthy!
console.log(typeof null); // object
JavaScript

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, concatenates
console.log(1 - '1'); // 0 — - always coerces to number
console.log([5] - 1); // 4 — [5] coerces to the number 5 first
console.log([] + []); // '' — both sides coerce to '', then concatenate

Number('') เป็น 0 ไม่ใช่ NaN สตริงว่างหรือมีแต่ whitespace จะแปลงเป็น 0 ด้วย Number() แต่ parseInt('') ให้ NaN — ทั้งสองฟังก์ชันไม่ตรงกันสำหรับ input ว่าง:

console.log(Number('')); // 0
console.log(Number(' ')); // 0
console.log(parseInt('')); // NaN

parseInt ที่ไม่มี 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 engines
console.log(parseInt('0x10')); // 16 — auto-detected as hex, radix defaulted to 16
console.log(parseInt('10', 2)); // 2 — radix 2: '10' means binary two
console.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([])); // true
console.log(Array.isArray({})); // false
console.log(typeof []); // 'object' — no help here

?. 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 missing
console.log(config.retries ?? 3); // 0 — RIGHT: ?? only falls back on null/undefined
console.log(config.timeout ?? 5000); // 5000 — timeout is genuinely absent
console.log(config.user?.profile?.email); // undefined — no throw, even though user is missing
flowchart 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
|| fall through สำหรับ falsy value ใดๆ; ?? fall through เฉพาะ null/undefined

การ 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 independent

JSON.parse(JSON.stringify(obj)) เป็นเทคนิค deep-clone ad hoc ที่ใช้กันบ่อย แต่มันทิ้งค่า undefined และ function ไปเงียบๆ และเปลี่ยน Date object เป็นสตริงธรรมดา — structuredClone() จัดการสิ่งเหล่านี้ได้ถูกต้องทั้งหมดและควรเลือกใช้แทน

JavaScript

operator ที่ coerce แต่ละตัวตอบคำถามที่ต่างกันเล็กน้อย: if ถาม “นี่ truthy ไหม?”, + ถาม “ฝั่งใดฝั่งหนึ่งดูเหมือนสตริงไหม?”, -/Number() ถาม “นี่คือเลขอะไร?” และ ?? ถาม “นี่คือ null หรือ undefined จริงๆ ไหม?” ไม่มีตัวไหนถาม “ผู้เรียกให้ค่าที่มีความหมายมาจริงไหม?” — คำถามนั้นเป็นคำถามที่ 0, '' และ NaN ตอบให้คุณไม่ได้เลย เพราะมันเป็นค่าที่ถูกต้องแต่บังเอิญเป็น falsy การอ่านโค้ดที่มี coercion เยอะๆ ให้เข้าใจดี หมายถึงการลอง substitute 0, '', [] และ {} เข้าไปในใจก่อนที่จะเชื่อ truthy check ทุกตัว

ChoiceBenefitCost
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 เล็กน้อย
  • “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() แทน

รูปแบบที่พบบ่อยมากในโค้ดที่ 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”

typeof null คืนค่าอะไรใน JavaScript?
ค่าใดต่อไปนี้เป็น falsy?
ผลลัพธ์ของ null == undefined คืออะไร?
โค้ดนี้พิมพ์อะไร?
const config = { retries: 0 };
console.log(config.retries || 3);
console.log(config.retries ?? 3);
โค้ดนี้พิมพ์อะไร และทำไมถึงเป็น bug?
function renderBadge(user) {
  if (user.count) {
    return user.count + ' items';
  }
  return 'No items yet';
}
console.log(renderBadge({ count: 0 }));