ทำความรู้จักกับ Prototypes พื้นฐานที่ซ่อนอยู่ใต้ JavaScript
เวลาเราเขียนโปรแกรมด้วยภาษา JavaScript เรามักจะได้ยินคำว่า Prototype (ต้นแบบของวัตถุ) กันอยู่บ่อยครั้ง หลายคนอาจจะรู้สึกว่ามันเป็นเรื่องยากหรือไกลตัว แต่จริงๆ แล้วมันคือหัวใจสำคัญที่ทำให้ JavaScript ทำงานได้แบบยืดหยุ่น ถ้าเปรียบเทียบให้เห็นภาพง่ายๆ Prototype เหมือนกับ "พิมพ์เขียวกลาง" ที่ทุกคนในตระกูลเดียวกันสามารถหยิบยืมความสามารถไปใช้ได้โดยไม่ต้องสร้างใหม่ซ้ำๆ
สมมติว่าเรากำลังสร้างโรงงานผลิตหุ่นยนต์ ถ้าเราต้องการให้หุ่นยนต์ทุกตัวเดินได้ แทนที่เราจะเขียนคำสั่งเดินเข้าไปในหุ่นยนต์ทุกตัวแยกกัน เราก็แค่เอาคำสั่งนั้นไปใส่ไว้ที่ Prototype ของหุ่นยนต์ตัวต้นแบบ เมื่อหุ่นยนต์ตัวใหม่ถูกสร้างขึ้นมา มันก็จะสามารถเรียกใช้คำสั่งเดินได้ทันทีเพราะมันเชื่อมโยงกับพิมพ์เขียวกลางตัวนั้นอยู่ นี่คือเหตุผลที่ JavaScript ใช้ระบบนี้เพื่อจัดการเรื่อง Inheritance (การสืบทอดคุณสมบัติ) ให้มีประสิทธิภาพ
การเข้าใจเรื่องนี้จะช่วยให้มือใหม่ก้าวข้ามจากการเขียนโค้ดแบบงูๆ ปลาๆ ไปสู่การออกแบบโปรแกรมที่สะอาดและประหยัดทรัพยากรมากขึ้น คุณจะได้รู้ว่าทำไมเราถึงสร้างฟังก์ชันครั้งเดียวแล้วเรียกใช้ได้หลายที่ และทำไมวัตถุต่างๆ ใน JavaScript ถึงมีความสามารถติดตัวมาตั้งแต่เกิด แม้เราจะไม่ได้เขียนโค้ดเพิ่มเข้าไปเลยก็ตาม
โครงสร้างของ Prototype และการสืบทอดคุณสมบัติ
ใน JavaScript ทุก Object (วัตถุที่เก็บข้อมูลและพฤติกรรม) จะมีช่องเชื่อมต่อพิเศษที่เรียกว่า Prototype ซ่อนอยู่ภายใน เมื่อเราพยายามเรียกใช้งานคุณสมบัติหรือคำสั่งที่ไม่มีอยู่ในตัววัตถุนั้น JavaScript จะไม่รีบบอกว่าหาไม่เจอ แต่มันจะวิ่งไปค้นหาที่ Prototype ให้โดยอัตโนมัติ กระบวนการนี้เรียกว่า Prototype Chain (ห่วงโซ่การค้นหาคุณสมบัติที่เชื่อมต่อกันเป็นทอดๆ)
เพื่อให้เห็นภาพชัดเจน เราลองมาดูการสร้างโครงสร้างผู้ใช้งานด้วย Constructor Function (ฟังก์ชันที่ใช้สร้างวัตถุใหม่) กันครับ เราจะกำหนดคุณสมบัติพื้นฐานไว้ที่ตัววัตถุ และกำหนดพฤติกรรมที่ใช้ร่วมกันไว้ที่ Prototype เพื่อให้ทุกตัวละครที่สร้างขึ้นมาสามารถเรียกใช้ได้ทันที
function User(name, role) {
// กำหนดคุณสมบัติที่แต่ละคนมีต่างกัน
this.name = name;
this.role = role;
}
// เพิ่มคำสั่งที่ใช้ร่วมกันไว้ใน Prototype
User.prototype.describe = function () {
return `${this.name} ทำงานเป็น ${this.role}`;
};
const ansh = new User("Ansh", "Developer");
console.log(ansh.describe()); // เรียกใช้คำสั่งที่อยู่ใน Prototype
ในโค้ดข้างต้น เราสร้างฟังก์ชัน User เพื่อรับค่าชื่อและตำแหน่ง ส่วน User.prototype.describe คือการฝากคำสั่ง describe ไว้ที่พิมพ์เขียวกลาง บรรทัดสุดท้ายเมื่อเราสั่ง ansh.describe() ตัว JavaScript จะหาคำสั่งนี้ในตัว ansh ก่อน ถ้าไม่เจอ มันจะวิ่งไปถามที่ User.prototype จนเจอผลลัพธ์
ผลลัพธ์ที่ได้จากการรันโค้ดชุดนี้คือ: Ansh ทำงานเป็น Developer ซึ่งจะเห็นได้ว่าแม้เราไม่ได้เขียนฟังก์ชัน describe ไว้ในตัวแปร ansh โดยตรง แต่ตัวแปรนี้ก็สามารถเรียกใช้มันได้ผ่านการสืบทอดนั่นเองครับ
การทำงานของ Property Lookup และข้อดีของการแชร์พฤติกรรม
หัวใจของการทำงานนี้คือสิ่งที่เรียกว่า Property Lookup (กระบวนการค้นหาข้อมูลในวัตถุ) เมื่อคุณเรียกใช้คำสั่งใดๆ JavaScript จะไล่ลำดับการหาเริ่มจาก "ตัววัตถุเอง" หากไม่พบ มันจะขยับขึ้นไปที่ Prototype ของมัน และถ้ายังไม่พบอีก มันจะขยับต่อไปเรื่อยๆ จนถึงจุดสูงสุดคือ Object.prototype (วัตถุต้นกำเนิดสูงสุดของทุกสิ่ง) หากหาไม่เจอจริงๆ ถึงจะคืนค่าเป็น undefined (ไม่มีค่า)
ข้อดีมหาศาลของการทำแบบนี้คือ "การประหยัดหน่วยความจำ" ลองจินตนาการว่าถ้าเรามีผู้ใช้งานหนึ่งล้านคน แล้วเราประกาศฟังก์ชันสำหรับสั่งงานไว้ในตัววัตถุทุกตัว พื้นที่ในหน่วยความจำของคอมพิวเตอร์จะบวมขึ้นมหาศาลโดยไม่จำเป็น การวางไว้ที่ Prototype จึงเป็นการสร้างฟังก์ชันขึ้นมาเพียงครั้งเดียว แต่ทุกคนสามารถใช้งานร่วมกันได้
เราลองมาเช็คความสัมพันธ์กันดูว่า วัตถุที่สร้างขึ้นมานั้นเชื่อมโยงกับ Prototype จริงหรือไม่ โดยใช้คำสั่ง Object.getPrototypeOf (คำสั่งดึงค่าต้นแบบของวัตถุออกมาดู) เพื่อตรวจสอบความถูกต้องของห่วงโซ่นี้
console.log(Object.getPrototypeOf(ansh) === User.prototype);
// ผลลัพธ์: true แสดงว่า ansh เชื่อมกับ User.prototype จริงๆ
console.log(ansh.hasOwnProperty("name"));
// ผลลัพธ์: true เพราะ name ถูกกำหนดไว้ในตัว ansh เอง
console.log(ansh.hasOwnProperty("describe"));
// ผลลัพธ์: false เพราะ describe เป็นของที่ยืมมาจาก Prototype
จากโค้ดชุดนี้ บรรทัดแรกยืนยันว่า ansh เชื่อมต่อกับพิมพ์เขียว User จริง บรรทัดถัดมา hasOwnProperty (คำสั่งเช็คว่าวัตถุมีข้อมูลนั้นจริงๆ หรือไม่) จะช่วยให้เราแยกแยะได้ว่าข้อมูลไหนเป็นของตัวเอง และข้อมูลไหนเป็นของที่ยืมมาจาก Prototype ครับ
การใช้ Class และความสัมพันธ์กับ Prototype ในปัจจุบัน
หลายคนอาจจะคุ้นเคยกับคำว่า Class (แม่แบบในการสร้างวัตถุ) ในภาษาอื่นมากกว่า ซึ่ง JavaScript ในปัจจุบันก็มี Class ให้ใช้แล้วเพื่อให้เขียนโค้ดได้อ่านง่ายขึ้น แต่ต้องจำไว้ว่า Class ใน JavaScript ไม่ใช่สิ่งใหม่ที่มาแทนที่ Prototype แต่มันเป็นเพียง "หน้ากาก" ที่ครอบทับระบบ Prototype เดิมเอาไว้เท่านั้น
เมื่อคุณเขียน Class ในปัจจุบัน เบื้องหลังการทำงาน JavaScript จะแปลงมันกลับไปเป็น Prototype เหมือนเดิมทุกประการ การเข้าใจพื้นฐานนี้จะทำให้คุณไม่สับสนเวลาไปเจอโค้ดรุ่นเก่า หรือเวลาต้องแก้บั๊กในโปรเจกต์ที่ใช้โครงสร้างแบบดั้งเดิม การรู้วิธีการทำงานภายในจะทำให้คุณเป็นโปรแกรมเมอร์ที่แก้ปัญหาได้ลึกกว่าคนอื่น
มาดูตัวอย่างการเขียนด้วย Class เทียบกับวิธีเดิมเพื่อให้เห็นว่าผลลัพธ์นั้นเหมือนกันเป๊ะ แม้หน้าตาโค้ดจะดูทันสมัยขึ้นมากก็ตาม
class Developer {
constructor(name) {
this.name = name;
}
code() {
return `${this.name} กำลังเขียนโค้ด`;
}
}
const dev = new Developer("พี่เลี้ยง");
console.log(dev.code()); // ผลลัพธ์: พี่เลี้ยง กำลังเขียนโค้ด
การใช้ class ช่วยให้เราจัดกลุ่มโค้ดได้เป็นระเบียบมากขึ้น constructor (ฟังก์ชันสำหรับตั้งค่าเริ่มต้น) ทำหน้าที่เหมือนฟังก์ชัน User ในตัวอย่างแรก ส่วน code() ก็ถูกนำไปวางไว้ใน Prototype ของ Developer โดยอัตโนมัติ ทำให้โค้ดดูสะอาดและทันสมัยขึ้นมากครับ
ข้อควรระวังและจุดที่มือใหม่มักพลาด
จุดที่มือใหม่มักพลาดบ่อยที่สุดคือเรื่อง Property Shadowing (การที่ข้อมูลในตัววัตถุไปบังข้อมูลใน Prototype) ถ้าคุณเผลอกำหนดชื่อตัวแปรที่ซ้ำกับชื่อฟังก์ชันใน Prototype ตัว JavaScript จะหยิบใช้ตัวที่อยู่ในวัตถุตัวเองก่อนเสมอ ซึ่งอาจทำให้ฟังก์ชันที่ตั้งใจจะใช้จาก Prototype ถูกเมินไปโดยปริยาย
อีกเรื่องที่ต้องระวังคือการแก้ไข Prototype ของ Object พื้นฐาน เช่น Array.prototype หรือ Object.prototype การเพิ่มฟังก์ชันเข้าไปในนี้อาจทำให้ไลบรารี (ชุดคำสั่งสำเร็จรูป) อื่นๆ ในโปรเจกต์ของคุณทำงานผิดพลาดได้ เพราะทุกคนต่างก็ใช้ Prototype เหล่านี้ร่วมกันในระบบ การแก้ไขโดยไม่ระวังจึงสร้างผลกระทบเป็นวงกว้าง
คำแนะนำคือให้สร้าง Prototype ของตัวเอง หรือใช้ Class ตามรูปแบบมาตรฐานดีกว่าครับ อย่าพยายามไปแก้ไขโครงสร้างหลักของภาษาหากไม่จำเป็นจริงๆ เพราะการรักษาความปลอดภัยของโค้ด (Code Integrity) เป็นสิ่งสำคัญมากในการทำงานร่วมกับทีม
สรุป: ทำไมต้องเข้าใจ Prototypes ให้ลึกซึ้ง
การเข้าใจ Prototype ไม่ได้มีไว้เพื่อให้คุณไปเขียนโค้ดที่ซับซ้อนเกินจำเป็น แต่เพื่อให้คุณเข้าใจว่า JavaScript ทำงานอย่างไรภายใต้ความสวยงามของ Class และ Syntax (รูปแบบการเขียนโค้ด) ใหม่ๆ เมื่อคุณเจอปัญหาเรื่องข้อมูลไม่แสดงผล หรือคำสั่งทำงานผิดพลาด การไล่ดู Prototype Chain จะช่วยให้คุณเห็นต้นตอของปัญหาได้รวดเร็วขึ้นมาก
สำหรับคนที่กำลังฝึกฝนเพื่อเป็นโปรแกรมเมอร์ นี่คือทักษะที่จะแยกคุณออกจากคนที่แค่ "เขียนตามตัวอย่าง" ไปสู่คนที่ "เข้าใจระบบงานจริง" ในการทำงานจริง คุณอาจไม่ได้นั่งสร้าง Prototype เองทุกวัน แต่การอ่านโค้ดของคนอื่น หรือการใช้ฟังก์ชันที่ติดมากับภาษาอย่าง map หรือ filter ก็ล้วนทำงานผ่านระบบนี้ทั้งสิ้น
จงจำไว้ว่าพื้นฐานที่แน่นคืออาวุธที่ทรงพลังที่สุด วันนี้คุณอาจจะมองว่า Prototype เป็นเรื่องไกลตัว แต่เมื่อคุณต้องเข้าไปจัดการกับโครงสร้างโปรเจกต์ขนาดใหญ่ หรือต้องทำการปรับแต่งประสิทธิภาพ (Optimization) ความรู้เรื่องนี้จะเป็นตัวช่วยที่ทำให้คุณก้าวผ่านความท้าทายไปได้อย่างมั่นใจครับ ขอให้สนุกกับการเขียนโค้ดและหมั่นทดลองรันตัวอย่างบ่อยๆ แล้วคุณจะเก่งขึ้นเองครับ
ที่มา: Understanding Prototypes in JavaScript: A Practical Guide for Developers — DEV Community