การเลือกใช้ระหว่าง var, let, และ const มีผลต่อความชัดเจน ความปลอดภัย และบั๊กของโค้ดคุณ โดยทั้งสามต่างกันที่ “ขอบเขตการมองเห็น (scope)”, “พฤติกรรมการ hoist”, และ “กฎการประกาศซ้ำ/แก้ค่า”.
ขอบเขต (scope)
- var: มีขอบเขตแบบฟังก์ชัน (function scope) หรือโกลบอล หากประกาศนอกฟังก์ชันจะเป็นตัวแปรโกลบอล และ var ไม่เคารพขอบเขตบล็อก { } เช่นใน if/for ตัวแปรยังมองเห็นได้นอกบล็อกนั้น.
- let / const: มีขอบเขตแบบบล็อก (block scope) คือมองเห็นได้ภายใน { } เท่านั้น เหมาะกับการจำกัดขอบเขตให้ชัดเจน ลดการชนกันของตัวแปร.
ตัวอย่าง:
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1
console.log(b); // ReferenceError
console.log(c); // ReferenceError
Hoisting และ Temporal Dead Zone (TDZ)
- var: ถูก hoist ขึ้นไปบนสุดของ scope และถูกกำหนดค่าเริ่มต้นเป็น undefined ทำให้สามารถอ้างถึงตัวแปรก่อนบรรทัดประกาศได้ (แม้จะได้ค่า undefined) ซึ่งอาจนำไปสู่บั๊กที่ตรวจจับยาก.
- let / const: ถูก hoist เช่นกัน แต่จะอยู่ใน TDZ จนกว่าจะถึงบรรทัดประกาศ การอ้างถึงก่อนประกาศจะเกิด ReferenceError ช่วยป้องกันการใช้ตัวแปรก่อนพร้อมใช้งาน.
ตัวอย่าง:
console.log(x); // undefined (var)
var x = 10;
console.log(y); // ReferenceError (let ใน TDZ)
let y = 10;
console.log(z); // ReferenceError (const ใน TDZ)
const z = 10;
การประกาศซ้ำและการแก้ค่า
- var: สามารถประกาศซ้ำชื่อเดิมใน scope เดียวกันได้ และแก้ค่าได้ ทำให้เสี่ยงเขียนทับโดยไม่ตั้งใจ.
- let: แก้ค่าได้ แต่ “ห้าม” ประกาศซ้ำใน block เดิม ลดความสับสนของชื่อซ้ำ.
- const: “ห้าม” reassignment หลังตั้งค่าเริ่มต้น และ “ห้าม” ประกาศซ้ำใน block เดิม อย่างไรก็ตาม สำหรับอ็อบเจ็กต์/อาเรย์ที่ประกาศด้วย const คุณยังสามารถ “แก้ไขค่าภายใน” ได้ (mutation) แต่เปลี่ยนตัวแปรให้ชี้ไปยังอ็อบเจ็กต์ใหม่ไม่ได้
ตัวอย่าง:
// var: ประกาศซ้ำได้
var a = 1;
var a = 2; // OK
// let: ห้ามประกาศซ้ำ
let b = 1;
// let b = 2; // SyntaxError
// const: ห้าม reassignment
const c = { name: "Ada" };
c.name = "Grace"; // OK (mutation)
c = { name: "Linus" }; // TypeError (reassignment)
พฤติกรรมในลูปและ closure
- var ใน for: ใช้ตัวแปรเดียวร่วมกันทุก iteration จนมักทำให้ callback หลังลูปเห็นค่าเดียวกัน (เช่นค่า iteration สุดท้าย).
- let ใน for: สร้าง binding ใหม่ต่อ iteration จึงทำให้ค่าแต่ละรอบเป็นอิสระ เหมาะกับการใช้ร่วมกับ callback/async ภายในลูป.
ตัวอย่าง:
// var: มักพังใน callback หลังลูป
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var i:", i), 0);
}
// Output: var i: 3, var i: 3, var i: 3
// let: ทำงานถูกต้องต่อรอบ
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let j:", j), 0);
}
// Output: let j: 0, let j: 1, let j: 2
แนวทางปฏิบัติที่แนะนำ
- ใช้ const เป็นค่าเริ่มต้น: หากตัวแปรไม่จำเป็นต้องเปลี่ยน ให้ใช้ const เพื่อสื่อเจตนาและป้องกันการแก้ค่าโดยไม่ตั้งใจ.
- ใช้ let เมื่อจำเป็นต้องแก้ค่า: เช่น ตัวแปรตัวนับลูป หรือตัวแปรที่ขึ้นกับเงื่อนไขหลายแบบภายในบล็อก.
- หลีกเลี่ยง var ในโค้ดสมัยใหม่: เพราะ scope กว้างและ hoisting เป็น undefined เพิ่มความเสี่ยงบั๊ก โดยทั่วไป let/const ให้พฤติกรรมคาดเดาง่ายกว่า.
ตารางเปรียบเทียบสรุป
| คุณสมบัติ | var | let | const |
| ขอบเขต | ฟังก์ชัน/โกลบอล | บล็อก | บล็อก |
| Hoisting | hoist เป็น undefined | hoist แต่มี TDZ | hoist แต่มี TDZ |
| ประกาศซ้ำ | ได้ | ไม่ได้ในบล็อกเดียวกัน | ไม่ได้ในบล็อกเดียวกัน |
| แก้ค่า | ได้ | ได้ | ไม่ได้ (คำาอ้างอิง), แต่ mutate โครงสร้างได้ |
| ใช้ในลูป | มีปัญหา closure บ่อย | ปลอดภัยต่อรอบ | ปลอดภัยต่อรอบ (ถ้าไม่ต้องเปลี่ยนตัวแปร) |
สรุป
ใช้ const โดยค่าเริ่มต้น, เปลี่ยนเป็น let เมื่อจำเป็นต้อง reassignment, หลีกเลี่ยง var ในโค้ด ES6+ เพื่อ scope ที่คาดเดาได้และลดบั๊ก.