JavaScript
Error Handling
ReferenceError — การอ้างอิงตัวแปรที่ไม่มีอยู่
ReferenceError เกิดเมื่ออ้างอิงตัวแปรที่ยังไม่ประกาศ — สะกดผิด, TDZ, หรือใช้ตัวแปรนอก scope
ReferenceError คืออะไร
ReferenceError เป็น Error ประเภทหนึ่งที่เกิดเมื่อ**อ้างอิงตัวแปรที่ยังไม่เคยประกาศใน scope ที่เข้าถึงได้ในขณะนั้น** จุดที่ทำให้ ReferenceError ต่างจาก Error ประเภทอื่นคือ **ตัวแปรนั้นไม่มีอยู่เลยใน scope ปัจจุบัน** — JavaScript มองหาชื่อตัวแปรใน scope chain แล้วไม่เจอ คิดง่าย ๆ: JavaScript ต้องรู้จักตัวแปรก่อนถึงจะใช้งานมันได้ — เหมือนการเรียกชื่อเพื่อนในห้อง ถ้าเพื่อนคนนั้นไม่อยู่ในห้องเลย ก็เรียกเท่าไหร่ก็ไม่มีใครตอบ เมื่อเจอ ReferenceError JavaScript จะ: 1. สร้าง Error object ที่มี `.name` = `"ReferenceError"` 2. แสดง `.message` ที่บอกว่าตัวแปรอะไรที่หาไม่เจอ พร้อมเหตุผล 3. **หยุดการทำงานที่บรรทัดนั้น** — โค้ดบรรทัดถัดไปใน scope เดียวกันจะไม่ทำงาน ReferenceError เกิด**ตอน runtime** (เหมือน TypeError ต่างจาก SyntaxError) — JavaScript อ่าน syntax ผ่านแล้ว แต่พอรันถึงบรรทัดที่อ้างอิงตัวแปรกลับหาไม่เจอ **ต่างจาก TypeError อย่างไร**: TypeError เกิดเพราะ**ใช้ค่าผิด type** — ตัวแปรมีอยู่จริง แต่ type ของมันไม่รองรับการดำเนินการนั้น (เช่นเรียก `"hello"()` — string ไม่ใช่ function) ส่วน ReferenceError เกิดเพราะ**ตัวแปรไม่มีอยู่เลย** — JavaScript ไม่รู้จักชื่อนั้นใน scope ไหนเลย
ReferenceError เกิดตอน runtime — JavaScript รู้ syntax แล้ว แต่พอรันถึงบรรทัดที่ใช้ตัวแปร กลับหาไม่เจอใน scope
// ❌ ใช้ตัวแปรที่ไม่เคยประกาศ — ReferenceError
console.log(price);
// ReferenceError: price is not defined
// .name = "ReferenceError"
// .message = "price is not defined"
// ^ price ไม่เคยถูกประกาศด้วย var, let, หรือ const — JavaScript หาไม่เจอเลย
console.log("บรรทัดนี้จะไม่ทำงาน");
// ^ ReferenceError หยุดการทำงานที่บรรทัด console.log(price)
// โค้ดบรรทัดถัดไปใน scope เดียวกันจะไม่ทำงาน- ReferenceError เกิดเมื่ออ้างอิงตัวแปรที่**ไม่เคยประกาศใน scope ที่เข้าถึงได้** — JavaScript มองหาชื่อตัวแปรใน scope chain แล้วไม่เจอ
- ReferenceError เกิดตอน **runtime** — โค้ด syntax ถูกต้องแล้ว แต่ตอนรันจริงตัวแปรไม่มีอยู่ (เหมือน TypeError ต่างจาก SyntaxError)
- `.name` = `"ReferenceError"` — บอกว่าเป็น error เกี่ยวกับการอ้างอิง (reference) ตัวแปรที่ไม่มีอยู่
- ReferenceError **หยุดการทำงานที่บรรทัดที่เกิด error** — โค้ดก่อนหน้าทำงานแล้ว แต่โค้ดถัดไปใน scope เดียวกันจะไม่ทำงาน
- ต่างจาก TypeError: TypeError เกิดเพราะ**ใช้ค่าผิด type** (ตัวแปรมีอยู่แต่ type ไม่ตรง), ReferenceError เกิดเพราะ**ตัวแปรไม่มีอยู่เลย** ใน scope
กรณีที่พบบ่อยของ ReferenceError
ReferenceError เกิดได้หลายสาเหตุ — แต่ทั้งหมดมีรากฐานเดียวกันคือ **JavaScript หาชื่อตัวแปรใน scope ที่เข้าถึงได้ในขณะนั้นไม่เจอ** มาดู 5 กรณีที่พบบ่อยที่สุด — แต่ละกรณีจะมีทั้งตัวอย่างที่ผิด (❌) และวิธีแก้ (✅) ให้เห็นภาพชัดเจน ทุกตัวอย่างด้านล่างนี้โค้ดก่อนหน้าทำงานได้ปกติ — จะเกิด ReferenceError เฉพาะตรงบรรทัดที่อ้างอิงตัวแปรที่ไม่มีอยู่
สาเหตุพื้นฐานที่สุด — ใช้ชื่อตัวแปรที่ไม่เคยถูกประกาศด้วย var, let, หรือ const มาก่อนเลย
// ❌ ใช้ตัวแปรที่ไม่เคยประกาศ — ReferenceError
console.log(price);
// ReferenceError: price is not defined
// ^ ตัวแปร price ไม่เคยถูกประกาศด้วย var, let, หรือ const เลย
// ✅ ประกาศตัวแปรก่อนใช้งาน
let price = 299;
console.log(price); // 299 — ทำงานได้ปกติ
// ✅ ประกาศด้วย const ก็ได้ (ถ้าไม่ต้องเปลี่ยนค่า)
const taxRate = 0.07;
console.log(taxRate); // 0.07 — ทำงานได้ปกติJavaScript เป็น case-sensitive — ตัวพิมพ์ใหญ่-พิมพ์เล็กคือคนละตัวแปรกัน สะกดผิดแค่ตัวเดียวก็เกิด ReferenceError
// ❌ สะกดผิด — JavaScript มองว่าเป็นคนละตัวแปร
let userName = "สมชาย";
console.log(username);
// ReferenceError: username is not defined
// ^ ประกาศ userName (ตัว N ใหญ่) แต่ใช้ username (ตัว n เล็กทั้งหมด)
// JavaScript เป็น case-sensitive — userName กับ username คือคนละตัวแปร
// ❌ อีกตัวอย่าง: สลับตัวอักษร
let productCode = "P001";
console.log(productCode);
// ReferenceError: productCode is not defined
// ^ ประกาศ productCode แต่ใช้ productCode — สลับ c กับ o
// ✅ ใช้ชื่อให้ตรงกันทุกตัวอักษร
let userName2 = "สมชาย";
console.log(userName2); // "สมชาย"
let productCode2 = "P001";
console.log(productCode2); // "P001"
// 💡 ใช้ autocomplete ใน editor — พิมพ์แค่ 2-3 ตัวแรกแล้วเลือกจาก list จะลดการสะกดผิดตัวแปรที่ประกาศใน function หรือ block จะมองเห็นได้เฉพาะใน scope นั้น — ใช้ข้างนอกจะเกิด ReferenceError
// ❌ ตัวแปรใน function — ใช้ข้างนอกไม่ได้
function greet() {
let message = "สวัสดี";
console.log(message); // "สวัสดี" — ใน function ใช้ได้
}
greet();
console.log(message);
// ReferenceError: message is not defined
// ^ message อยู่ใน scope ของ function greet() — ข้างนอกเข้าไม่ถึง
// ❌ ตัวแปรใน block {} (let/const) — ใช้ข้างนอก block ไม่ได้
{
let secret = "รหัสลับ";
console.log(secret); // "รหัสลับ" — ใน block ใช้ได้
}
console.log(secret);
// ReferenceError: secret is not defined
// ^ secret อยู่ใน block scope — ออกมานอก {} แล้วเข้าไม่ถึง
// ✅ ประกาศตัวแปรใน scope ที่จะใช้ — หรือประกาศข้างนอกให้ scope ชั้นในใช้
let message2 = "สวัสดี";
function greet2() {
console.log(message2); // "สวัสดี" — inner scope เข้าถึง outer scope ได้
}
greet2();
console.log(message2); // "สวัสดี" — ใช้ได้ทั้งในและนอก functionlet และ const มี Temporal Dead Zone (TDZ) — ช่วงระหว่างเริ่ม block จนถึงบรรทัดที่ประกาศ ใช้ตัวแปรในช่วงนี้จะเกิด ReferenceError
// ❌ ใช้ let ก่อนประกาศ — ReferenceError จาก TDZ
console.log(name);
// ReferenceError: Cannot access 'name' before initialization
let name = "สมชาย";
// ^ let/const มี TDZ — ใช้ก่อนบรรทัดประกาศไม่ได้
// error message: "Cannot access 'name' before initialization"
// ❌ ใช้ const ก่อนประกาศ — TDZ เช่นเดียวกัน
console.log(MAX_SIZE);
// ReferenceError: Cannot access 'MAX_SIZE' before initialization
const MAX_SIZE = 100;
// ^ const ก็มี TDZ เหมือน let
// ✅ ประกาศก่อนใช้งานเสมอ
let name2 = "สมชาย";
console.log(name2); // "สมชาย" — ทำงานได้ปกติ
const MAX_SIZE2 = 100;
console.log(MAX_SIZE2); // 100 — ทำงานได้ปกติvar ถูก hoist และมีค่าเริ่มต้นเป็น undefined (ไม่เกิด ReferenceError) — แต่ let/const ถูก hoist โดยไม่มีค่าเริ่มต้น (เกิด ReferenceError ถ้าใช้ก่อน)
// ✅ var — ใช้ก่อนประกาศ ไม่เกิด ReferenceError (แต่ค่าเป็น undefined)
console.log(score);
// undefined — ไม่เกิด error!
var score = 85;
// ^ var ถูก hoist — JavaScript รู้ว่ามีตัวแปร score แต่ยังไม่ได้กำหนดค่า
// ค่าจึงเป็น undefined — ไม่อ่าน error แต่อาจทำให้ logic ผิดได้
// ❌ let — ใช้ก่อนประกาศ เกิด ReferenceError
console.log(level);
// ReferenceError: Cannot access 'level' before initialization
let level = 10;
// ^ let ถูก hoist เหมือน var แต่ไม่มีค่าเริ่มต้น — อยู่ใน TDZ จนถึงบรรทัดประกาศ
// ✅ ใช้ let/const แบบถูกต้อง — ประกาศก่อนใช้
let level2 = 10;
console.log(level2); // 10 — ทำงานได้ปกติ
// 💡 ความต่างสำคัญ:
// var → hoist + กำหนด undefined → ไม่มี ReferenceError (แต่อันตรายเพราะซ่อน bug)
// let → hoist + เข้า TDZ → ReferenceError ถ้าใช้ก่อน (ปลอดภัยกว่า — เจอ bug เร็ว)
// const → hoist + เข้า TDZ → ReferenceError ถ้าใช้ก่อน (เหมือน let)- **ใช้ตัวแปรที่ยังไม่ประกาศ** — สาเหตุพื้นฐานที่สุด ประกาศด้วย `let`, `const`, หรือ `var` ก่อนใช้งานเสมอ
- **สะกดชื่อตัวแปรผิด** — JavaScript เป็น case-sensitive (`userName` กับ `username` คือคนละตัว), ใช้ autocomplete ใน editor ช่วยลดการพิมพ์ผิด
- **ใช้ตัวแปรนอก scope** — ตัวแปรใน function/block มองเห็นเฉพาะใน scope นั้น, scope ชั้นในใช้ของชั้นนอกได้ แต่ชั้นนอกใช้ของชั้นในไม่ได้
- **TDZ ของ let/const** — ใช้ `let`/`const` ก่อนบรรทัดประกาศจะเกิด ReferenceError: `"Cannot access before initialization"` — ประกาศก่อนใช้เสมอ
- **var vs let/const** — `var` ใช้ก่อนประกาศได้ (ค่าเป็น undefined ไม่เกิด ReferenceError), `let`/`const` ใช้ก่อนไม่ได้ (เกิด ReferenceError — ปลอดภัยกว่าเพราะเจอ bug เร็ว)
ReferenceError กับ scope
สาเหตุที่พบบ่อยที่สุดของ ReferenceError คือ**การเข้าใจ scope ผิด** — ใช้ตัวแปรผิดที่เพราะคิดว่ามันมองเห็นได้จากทุกที่ scope คือขอบเขตที่ตัวแปรสามารถถูกเข้าถึงได้ — JavaScript มี scope หลายระดับ และกฎการเข้าถึงมีทิศทางเดียว: **scope ชั้นในเข้าถึง scope ชั้นนอกได้ แต่ scope ชั้นนอกเข้าถึง scope ชั้นในไม่ได้** มาทำความเข้าใจ ReferenceError ที่เกิดจาก scope แต่ละรูปแบบให้ชัดเจน:
ตัวแปรที่ประกาศใน function เป็นของ function นั้น — ภายนอก function ไม่มีทางเข้าถึงได้
// ❌ ใช้ตัวแปรใน function จากข้างนอก — ReferenceError
function calculateTotal() {
let discount = 50; // ตัวแปร discount อยู่ใน function scope
console.log("ใน function:", discount); // 50
}
calculateTotal();
console.log(discount);
// ReferenceError: discount is not defined
// ^ discount อยู่ใน scope ของ calculateTotal() เท่านั้น
// ❌ function expression ก็เช่นกัน
const getMessage = function() {
let text = "hello";
return text;
};
console.log(getMessage()); // "hello"
console.log(text);
// ReferenceError: text is not defined
// ^ text อยู่ใน function scope — ข้างนอกเข้าไม่ถึง
// ✅ ทำให้ตัวแปรมองเห็นจากข้างนอก — ประกาศใน scope ที่กว้างกว่า
let discount2 = 50;
function calculateTotal2(price) {
console.log("ใน function:", discount2); // 50 — เข้าถึงได้ (outer scope)
return price - discount2;
}
console.log(calculateTotal2(500)); // 450
console.log("นอก function:", discount2); // 50 — ใช้ได้ทั้งในและนอก functionlet และ const ยึดติดกับ block `{}` ที่ประกาศ — var ไม่ยึดติด (var เป็น function scope)
// ❌ let/const ใน block — ใช้ข้างนอก block ไม่ได้
if (true) {
let blockVar = "ใน if";
const BLOCK_CONST = 100;
console.log(blockVar); // "ใน if" — ใน block ใช้ได้
}
console.log(blockVar);
// ReferenceError: blockVar is not defined
// ^ let อยู่ใน block scope ของ if — ออกนอก {} แล้วหมดอายุ
// ❌ const ก็เช่นกัน
console.log(BLOCK_CONST);
// ReferenceError: BLOCK_CONST is not defined
// ✅ var ใน block — ใช้ข้างนอกได้ (var ไม่มี block scope)
if (true) {
var functionScoped = "var หลุดออกจาก block ได้";
}
console.log(functionScoped); // "var หลุดออกจาก block ได้"
// ^ var ไม่ยึด block scope — หลุดออกมาข้างนอกได้ (ไม่เกิด ReferenceError)
// 💡 let/const ปลอดภัยกว่าเพราะควบคุม scope ได้ดี
// var ใช้ข้างนอก block ได้ แต่อาจทำให้ชื่อตัวแปรชนกันโดยไม่ตั้งใจscope ซ้อนกันเหมือนกล่อง — กล่องในมองเห็นของในกล่องนอก แต่กล่องนอกมองไม่เห็นของในกล่องใน
// ✅ scope ชั้นในเข้าถึง scope ชั้นนอกได้
let outer = "ฉันอยู่ข้างนอก";
function showScope() {
let inner = "ฉันอยู่ข้างใน";
console.log(outer); // "ฉันอยู่ข้างนอก" — inner scope เข้าถึง outer scope ได้
console.log(inner); // "ฉันอยู่ข้างใน" — เข้าถึงตัวแปรของตัวเองได้
}
showScope();
// ❌ scope ชั้นนอกเข้าถึง scope ชั้นในไม่ได้
console.log(inner);
// ReferenceError: inner is not defined
// ^ inner อยู่ใน function scope ของ showScope() — ข้างนอกเข้าไม่ถึง
// ตัวอย่าง scope ซ้อนหลายชั้น:
let level1 = "ชั้น 1";
function outerFn() {
let level2 = "ชั้น 2";
function innerFn() {
let level3 = "ชั้น 3";
console.log(level1); // "ชั้น 1" — เข้าถึงได้ (scope chain: innerFn -> outerFn -> global)
console.log(level2); // "ชั้น 2" — เข้าถึงได้ (scope chain: innerFn -> outerFn)
console.log(level3); // "ชั้น 3" — ตัวแปรของตัวเอง
}
innerFn();
// console.log(level3); // ❌ ReferenceError — outerFn เข้าไม่ถึงตัวแปรใน innerFn
}
outerFn();
// console.log(level2); // ❌ ReferenceError — global เข้าไม่ถึงตัวแปรใน outerFn
// console.log(level3); // ❌ ReferenceError — global เข้าไม่ถึงตัวแปรใน innerFn- **Scope คือขอบเขตที่ตัวแปรมองเห็นได้** — ตัวแปรที่ประกาศใน scope หนึ่ง อาจมองไม่เห็นจากอีก scope หนึ่ง
- **Function scope**: ตัวแปรที่ประกาศใน function (`var`, `let`, `const`) มองเห็นเฉพาะใน function นั้น — ใช้ข้างนอกไม่ได้
- **Block scope**: `let` และ `const` ใน `{}` มองเห็นเฉพาะใน block นั้น — `var` ไม่มี block scope (ใช้ข้างนอก `{}` ได้ ทำให้สับสนง่าย)
- **Scope chain ทิศทางเดียว**: scope ชั้นใน → มองเห็น scope ชั้นนอก, แต่ scope ชั้นนอก → มองไม่เห็น scope ชั้นใน
- **Global scope**: ตัวแปรที่ประกาศนอก function/block ทุกอัน — ทุก scope มองเห็นได้ (แต่ควรใช้เท่าที่จำเป็น)
TDZ (Temporal Dead Zone) กับ ReferenceError
**Temporal Dead Zone (TDZ)** คือช่วงเวลาระหว่างที่ JavaScript รู้ว่ามีตัวแปรอยู่ (จากการ hoist) กับช่วงที่ตัวแปรนั้นถูกกำหนดค่าจริง ๆ — ในช่วง TDZ การใช้ตัวแปรจะเกิด ReferenceError TDZ เป็นกลไกของ `let` และ `const` โดยเฉพาะ — `var` ไม่มี TDZ การเข้าใจ TDZ ช่วยให้เรา: - เข้าใจว่าทำไม `let`/`const` ถึงใช้ก่อนประกาศไม่ได้ (ทั้งที่ `var` ใช้ได้) - อ่าน error message `"Cannot access 'X' before initialization"` แล้วรู้ทันทีว่าเกิดจาก TDZ - เขียนโค้ดที่มีลำดับการประกาศที่ถูกต้อง
TDZ เริ่มตั้งแต่ `{` เปิด block จนถึงบรรทัดที่ประกาศ let/const — ใช้ตัวแปรในช่วงนี้ = ReferenceError
// TDZ ของ let ใน block — ภาพชัดเจน
{
// 👈 TDZ ของ name เริ่มตรงนี้ (เปิด block)
// name ถูก hoist แล้ว — JavaScript รู้ว่ามีตัวแปรนี้
// แต่ยังไม่มีค่าเลย — อยู่ใน TDZ
// console.log(name); // ❌ อยู่ใน TDZ
// ReferenceError: Cannot access 'name' before initialization
let name = "สมชาย";
// 👈 TDZ ของ name สิ้นสุดตรงนี้ — name มีค่า "สมชาย" แล้ว
console.log(name); // ✅ พ้น TDZ แล้ว — "สมชาย"
}
// TDZ ของหลายตัวแปรใน block เดียวกัน:
{
// TDZ ของ a เริ่ม — a ยังใช้ไม่ได้
// TDZ ของ b เริ่ม — b ยังใช้ไม่ได้
let a = 1; // TDZ ของ a จบ — a = 1
console.log(a); // 1 ✅
let b = 2; // TDZ ของ b จบ — b = 2
console.log(b); // 2 ✅
}
// ^ a ใช้ได้แค่หลังจากบรรทัด let a = 1
// b ใช้ได้แค่หลังจากบรรทัด let b = 2var ถูก hoist พร้อมกับกำหนดค่าเริ่มต้นเป็น undefined — จึงใช้ก่อนบรรทัดประกาศได้โดยไม่เกิด ReferenceError
// ✅ var — ใช้ก่อนประกาศ ไม่มี ReferenceError
console.log(color);
// undefined — var ถูก hoist + กำหนด undefined ให้
var color = "แดง";
console.log(color); // "แดง" — หลังจากประกาศมีค่าจริง
// เปรียบเทียบ: var vs let ในสถานการณ์เดียวกัน
// --- ตัวอย่าง 1: ใช้ var ---
console.log("var:", counter);
// "var: undefined" — ไม่ error
var counter = 10;
// --- ตัวอย่าง 2: ใช้ let (แยก block คนละอัน) ---
{
// console.log(newCounter); // ❌ ReferenceError
let newCounter = 10;
console.log("let:", newCounter); // "let: 10"
}
// 💡 var ไม่มี TDZ ฟังดูดี — แต่ความจริงคืออันตราย!
// เพราะ var ใช้ก่อนประกาศได้เงียบ ๆ — ทำให้ logic ผิดโดยไม่รู้ตัว
// เช่น: ใช้ตัวแปรก่อนกำหนดค่า → ได้ undefined → คำนวณผิด
// let/const บอกเราทันทีว่าลำดับการประกาศผิด → แก้ bug ง่ายกว่าparameter default value ก็อยู่ใน TDZ ของ parameter ตัวอื่นใน function เดียวกัน — การอ้างอิง parameter ก่อนกำหนดค่า = ReferenceError
// ❌ TDZ ใน parameter default value
function createUser(name = defaultName, defaultName = "ไม่ระบุ") {
console.log(name);
}
createUser();
// ReferenceError: Cannot access 'defaultName' before initialization
// ^ defaultName ถูกใช้เป็นค่า default ของ name — แต่ defaultName ยังอยู่ใน TDZ
// เพราะ defaultName ประกาศใน parameter ตัวถัดไป!
// ✅ สลับลำดับ parameter — ประกาศก่อนใช้
function createUser2(defaultName = "ไม่ระบุ", name = defaultName) {
console.log(name); // "ไม่ระบุ"
}
createUser2();
// ^ defaultName ประกาศก่อน — name ใช้ defaultName เป็นค่า default ได้
createUser2("สมชาย", "สมศรี");
// output: "สมศรี"
// ^ ถ้าส่งค่ามาทั้งสองตัว — ใช้ค่าที่ส่งมาแทน default
// 💡 กฎง่าย ๆ: parameter ที่จะถูกอ้างอิงเป็น default value
// ต้องอยู่ทางซ้าย (ประกาศก่อน) parameter ที่จะใช้ค่านั้น- **TDZ = Temporal Dead Zone** — ช่วงที่ตัวแปร `let`/`const` มีตัวตนแล้ว (จากการ hoist) แต่ยังไม่มีค่า — ใช้ในช่วงนี้ = ReferenceError: `"Cannot access before initialization"`
- **TDZ เริ่มที่ `{` เปิด block และจบที่บรรทัดประกาศตัวแปร** — `let x = 5` → ก่อนบรรทัดนี้คือ TDZ, หลังบรรทัดนี้ใช้ `x` ได้ปกติ
- **`var` ไม่มี TDZ** — ใช้ก่อนประกาศได้ (ค่าเป็น undefined) — แต่การไม่มี TDZ ทำให้บัคซ่อนอยู่ ไม่ใช่ข้อดี
- **`let`/`const` มี TDZ = ปลอดภัยกว่า** — JavaScript บอกทันทีว่าลำดับการประกาศผิด แทนที่จะให้ค่า undefined แบบเงียบ ๆ
- **TDZ ใน parameter default value** — parameter ทางขวาอยู่ใน TDZ ของ parameter ทางซ้าย — ประกาศ parameter ที่จะถูกอ้างอิงไว้ทางซ้ายก่อน
วิธีป้องกันและแก้ไข ReferenceError
ReferenceError เป็น error ที่**ป้องกันได้ 100%** — แค่ฝึกนิสัยประกาศตัวแปรก่อนใช้และสนใจขอบเขตของ scope ก็ลด ReferenceError ได้เกือบทั้งหมด นี่คือวิธีปฏิบัติที่ช่วยให้เจอ ReferenceError น้อยลงและแก้ไขได้เร็ว:
- **อ่าน error message ให้ละเอียด** — ReferenceError มี 2 รูปแบบหลัก: `"X is not defined"` = ตัวแปร X ไม่เคยประกาศใน scope นี้เลย, `"Cannot access 'X' before initialization"` = ตัวแปร X อยู่ใน TDZ — ใช้ `let`/`const` ก่อนบรรทัดประกาศ ดูข้อความแล้วไปดูบรรทัดที่ console บอก
- **ประกาศตัวแปรก่อนใช้งานเสมอ** — นิสัยง่าย ๆ แต่สำคัญที่สุด: `let`, `const`, `var` ต้องมาก่อนบรรทัดที่ใช้ตัวแปรนั้น — เขียน `const name = "สมชาย"` ไว้ด้านบนสุดของ scope ก่อนจะใช้ `name`
- **ตรวจสอบการสะกดชื่อตัวแปร** — JavaScript เป็น case-sensitive — `userName`, `username`, `UserName` คือ 3 ตัวแปรที่ต่างกัน ใช้ autocomplete ใน editor (พิมพ์ 2-3 ตัวแรกแล้วเลือกจาก list) จะลดการพิมพ์ผิด
- **ใช้ `console.log()` ตรวจสอบว่าตัวแปรประกาศหรือยัง** — ถ้าไม่แน่ใจว่าตัวแปรถูกประกาศใน scope นี้หรือไม่ ให้ `console.log(ตัวแปร)` ดูก่อน — ถ้าได้ ReferenceError แปลว่าตัวแปรไม่มีอยู่ตรงนี้
- **สนใจ scope ของตัวแปร** — ถามตัวเองว่า "ตัวแปรนี้ประกาศตรงไหน? scope นี้มองเห็นมันไหม?" — ตัวแปรใน function ใช้ข้างนอกไม่ได้, `let`/`const` ใน `{}` ใช้ข้างนอก block ไม่ได้
- **ใช้ `let` และ `const` แทน `var`** — `let`/`const` มี TDZ ทำให้รู้ทันทีถ้าใช้ก่อนประกาศ — `var` ไม่บอก (ได้ undefined แบบเงียบ ๆ) ทำให้หาบัคยากกว่า
- **ใช้ editor ที่ดี** — VS Code และ editor สมัยใหม่มี IntelliSense — จะแสดงชื่อตัวแปรที่มีอยู่ใน scope ปัจจุบันให้เลือก, ขีดเส้นแดงใต้ตัวแปรที่ไม่มีอยู่, และแสดง scope ของตัวแปร — ช่วยจับ ReferenceError ก่อนรัน
ReferenceError กับ Error ประเภทอื่น
มือใหม่มักสับสนว่า error ที่เจอเป็น ReferenceError, TypeError หรือ SyntaxError — มาดูตารางเปรียบเทียบให้เห็นความแตกต่างชัดเจน:
| Error ประเภท | เกิดตอน | สาเหตุ | ตัวอย่าง error message | วิธีแก้ |
|---|---|---|---|---|
| SyntaxError | Parsing (ก่อนรัน) | เขียน syntax ผิดกฎของภาษา — อ่านโค้ดแล้วไม่เข้าใจ | `Unexpected token`\n`Unexpected end of input` | ตรวจความถูกต้องของ syntax — วงเล็บครบคู่, string ปิด, ไม่มี reserved word ผิดที่ |
| TypeError | Runtime (ตอนรัน) | ใช้ค่าที่ type ไม่รองรับการดำเนินการนั้น — ตัวแปรมีอยู่แต่ใช้ผิดวิธี | `X is not a function`\n`Cannot read properties of null`\n`Assignment to constant variable` | ตรวจ type ด้วย `typeof` ก่อนใช้งาน, ใช้ default value, เช็ค null/undefined ก่อนใช้ `.` |
| ReferenceError | Runtime (ตอนรัน) | อ้างอิงตัวแปรที่ไม่มีอยู่ใน scope ที่เข้าถึงได้ — สะกดผิด, TDZ, หรือใช้ตัวแปรนอก scope | `X is not defined`\n`Cannot access 'X' before initialization` | ประกาศตัวแปรก่อนใช้งาน, ตรวจการสะกดชื่อ, สนใจ scope, ใช้ `let`/`const` เพื่อให้ TDZ ช่วยเตือน |
| RangeError | Runtime (ตอนรัน) | ค่าอยู่นอกช่วงที่ยอมรับได้ — เช่น จำนวนทศนิยมเกิน, array length ไม่ถูกต้อง, recursion ลึกเกิน | `Invalid array length`\n`Maximum call stack size exceeded` | ตรวจสอบว่าค่าที่ส่งให้ method อยู่ในช่วงที่ยอมรับได้ |
สังเกตว่า SyntaxError เกิด**ก่อนรัน** — โค้ดทั้งไฟล์ไม่ทำงานเลย ส่วน ReferenceError, TypeError และ RangeError เกิด**ตอนรัน** — โค้ดก่อนหน้าทำงานได้ แต่หยุดที่บรรทัดที่เกิด error วิธีแยก ReferenceError จาก TypeError ง่าย ๆ: - **ReferenceError**: ตัวแปร**ไม่มีอยู่เลย**ใน scope นี้ → `"X is not defined"` หรือ `"Cannot access before initialization"` - **TypeError**: ตัวแปร**มีอยู่**แต่ใช้มันผิดวิธี → `"X is not a function"` หรือ `"Cannot read properties of null"` จำง่าย ๆ: Reference = การอ้างอิง (ตัวแปรหาไม่เจอ), Type = ชนิดข้อมูล (ใช้ผิด type)
สรุป — ReferenceError
- **ReferenceError** คือ Error ที่เกิดเมื่ออ้างอิงตัวแปรที่ไม่มีอยู่ใน scope ที่เข้าถึงได้ — เกิดตอน **runtime** (ขณะโค้ดรัน)
- **สาเหตุหลัก 5 ประการ**: (1) ใช้ตัวแปรที่ยังไม่ประกาศ, (2) สะกดชื่อผิด (case-sensitive), (3) ใช้ตัวแปรนอก scope, (4) TDZ — ใช้ `let`/`const` ก่อนบรรทัดประกาศ, (5) `var` กับ `let`/`const` มีพฤติกรรม hoisting ต่างกัน
- **จุดสังเกต**: error message มี 2 รูปแบบ — `"X is not defined"` = ตัวแปรไม่เคยประกาศใน scope นี้, `"Cannot access 'X' before initialization"` = ตัวแปรอยู่ใน TDZ
- **Scope คือกุญแจ**: scope ชั้นในเข้าถึงชั้นนอกได้ แต่ชั้นนอกเข้าถึงชั้นในไม่ได้ — ตัวแปรใน function/block มองเห็นเฉพาะในขอบเขตของมัน
- **TDZ มีเฉพาะ `let`/`const`**: ช่วงระหว่างเปิด block จนถึงบรรทัดประกาศ — ใช้ในช่วงนี้ = ReferenceError — `var` ไม่มี TDZ (ใช้ก่อนได้เป็น undefined — แต่ทำให้หาบัคยาก)
- **ป้องกันได้ 100%**: ประกาศตัวแปรก่อนใช้, ตรวจสอบการสะกด, สนใจ scope, ใช้ `let`/`const` (TDZ ช่วยจับ bug), ใช้ `console.log` ตรวจสอบ, ใช้ editor ที่มี IntelliSense
- **ต่างจาก TypeError**: TypeError = ตัวแปรมีอยู่แต่ใช้ผิด type, ReferenceError = ตัวแปรไม่มีอยู่เลยใน scope — จำง่าย ๆ: Reference = การอ้างอิง (หาไม่เจอ), Type = ชนิดข้อมูล (ใช้ผิด)
| สถานการณ์ | ReferenceError หรือไม่ | วิธีแก้ |
|---|---|---|
| `console.log(x)` — x ไม่เคยประกาศ | ใช่ — ReferenceError | ประกาศ `x` ด้วย `let`, `const` หรือ `var` ก่อนใช้งาน |
| ใช้ `userName` ตอนประกาศ `username` | ใช่ — ReferenceError | ตรวจการสะกดชื่อตัวแปร — JavaScript เป็น case-sensitive |
| ใช้ตัวแปรที่ประกาศใน function จากข้างนอก function | ใช่ — ReferenceError | ประกาศตัวแปรใน scope ที่จะใช้ — หรือ return ค่าออกมาจาก function |
| ใช้ `let` ก่อนบรรทัดประกาศ | ใช่ — ReferenceError | ประกาศ `let`/`const` ก่อนใช้งาน — error message: `Cannot access before initialization` |
| ใช้ `var` ก่อนบรรทัดประกาศ | ไม่ใช่ — ไม่เกิด error (แต่ค่าเป็น undefined) | ไม่ควรใช้ `var` ก่อนประกาศแม้ไม่ error — ใช้ `let`/`const` ดีกว่า |
| `null.name` | ไม่ใช่ — TypeError | null ไม่มี property — เช็คว่าไม่เป็น null ก่อนใช้ `.` |
| ลืมปิดวงเล็บ `)` | ไม่ใช่ — SyntaxError | ตรวจวงเล็บให้ครบคู่ |
| ใช้ `const` ก่อนบรรทัดประกาศ | ใช่ — ReferenceError | `const` มี TDZ เหมือน `let` — ประกาศ `const` ก่อนใช้งาน |