คลาส `Object` ในภาษา JavaScript
บทความนี้อธิบายเกี่ยวกับคลาส Object ในภาษา JavaScript
บทความนี้อธิบายเกี่ยวกับคลาส Object ในภาษา JavaScript พร้อมตัวอย่างการใช้งานจริง
YouTube Video
คลาส Object ในภาษา JavaScript
Object คืออ็อบเจ็กต์ที่มีอยู่แล้วในตัว ซึ่งเป็นพื้นฐานของอ็อบเจ็กต์ทั้งหมดในภาษา JavaScript คุณสมบัติหลักของภาษาหลายอย่าง เช่น การจัดการพร็อพเพอร์ตี้ การสืบทอด (prototype chain) การนับรายการ การโคลน และการตรึงค่า ถูกจัดเตรียมผ่านพฤติกรรมของ Object
Object.create
มีหลายวิธีในการสร้างอ็อบเจ็กต์ คุณควรเลือกใช้ให้เหมาะสมกับวัตถุประสงค์
อ็อบเจ็กต์ลิเทอรัล (เป็นวิธีที่ใช้บ่อยที่สุด)
โค้ดด้านล่างนี้แสดงวิธีสร้างอ็อบเจ็กต์ที่ง่ายและอ่านเข้าใจได้ดีที่สุด
1// Create an object using object literal
2const user = {
3 name: "Alice",
4 age: 30,
5 greet() {
6 return `Hello, I'm ${this.name}`;
7 }
8};
9
10console.log(user.greet()); // "Hello, I'm Alice"
- ในตัวอย่างนี้ คุณสมบัติและเมธอดถูกกำหนดโดยใช้ลิเทอรัล มันเรียบง่ายและโดยทั่วไปให้ประสิทธิภาพที่ดีกว่า
คอนสตรัคเตอร์ new Object()
คอนสตรัคเตอร์ Object ใช้ไม่บ่อยนัก แต่ก็ควรเข้าใจพฤติกรรมของมัน
1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
new Object()จะคืนค่าอ็อบเจ็กต์เปล่า แต่การใช้{}สั้นกว่าและนิยมกว่า
การกำหนดโปรโตไทป์ด้วย Object.create
Object.create ใช้สำหรับสร้างอ็อบเจ็กต์โดยกำหนดโปรโตไทป์ได้
1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
Object.createเหมาะสำหรับออกแบบอ็อบเจ็กต์ที่ต้องการใช้การสืบทอด ทำให้คุณควบคุม prototype chain ได้ละเอียด
แอตทริบิวต์ของพร็อพเพอร์ตี้และตัวบรรยาย (descriptors)
พร็อพเพอร์ตี้มีแอตทริบิวต์เช่น 'value', 'writable', 'enumerable', และ 'configurable' ซึ่งสามารถควบคุมได้ละเอียดด้วย Object.defineProperty
ตัวอย่างพื้นฐานของการใช้ defineProperty
ถัดไปเป็นตัวอย่างของการกำหนดคุณสมบัติที่ไม่สามารถวนซ้ำได้และแบบอ่านอย่างเดียวโดยใช้ defineProperty
1// Define a non-enumerable read-only property
2const person = { name: "Carol" };
3
4Object.defineProperty(person, "id", {
5 value: 12345,
6 writable: false,
7 enumerable: false,
8 configurable: false
9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
- การใช้
definePropertyช่วยให้คุณควบคุมพฤติกรรมของคุณสมบัติได้อย่างแม่นยำ เช่น การวนซ้ำ การเขียนทับ และการลบ
พร็อพเพอร์ตี้แบบแอ็กเซสเซอร์ (getter / setter)
ด้วย accessors คุณสามารถใส่ลอจิกขณะอ่านหรือเขียนค่าพร็อพเพอร์ตี้ได้
1// Use getter and setter to manage internal state
2const data = {
3 _value: 1,
4 get value() {
5 return this._value;
6 },
7 set value(v) {
8 if (typeof v === "number" && v > 0) {
9 this._value = v;
10 } else {
11 throw new Error("value must be a positive number");
12 }
13 }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
- ด้วย
getterและsetterคุณสามารถเข้าถึงพร็อพเพอร์ตี้เหมือน API ภายนอก และสามารถเพิ่ม validation หรือ side effect ได้
โปรโตไทป์และการสืบทอด (prototype / __proto__ / Object.getPrototypeOf)
การสืบทอดในภาษา JavaScript อาศัย prototype chain ไม่ใช่คลาส อ็อบเจ็กต์สามารถอ้างถึงอ็อบเจ็กต์อื่นเป็นโปรโตไทป์ได้
Object.getPrototypeOf และ Object.setPrototypeOf
ตัวอย่างต่อไปนี้แสดงวิธีการตรวจสอบและตั้งค่าโปรโตไทป์
1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
Object.getPrototypeOfใช้ดึงโปรโตไทป์ของอ็อบเจ็กต์Object.setPrototypeOfเปลี่ยนโปรโตไทป์ของอ็อบเจ็กต์ที่มีอยู่ แต่ควรใช้อย่างระวังเพราะอาจส่งผลต่อประสิทธิภาพ
เมธอดสำคัญที่มีอยู่ในตัว
เราจะอธิบายอย่างชัดเจนเกี่ยวกับเมธอดที่ใช้บ่อยและสำคัญ ซึ่งคัดเลือกมาจากเมธอดอินสแตนซ์ของ Object.prototype และเมธอดแบบสแตติกที่เป็นของ Object
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnProperty, isPrototypeOf, toString และ valueOf กำหนดพฤติกรรมพื้นฐานของอ็อบเจ็กต์
1// Demonstrate prototype methods
2const base = { greet() { return "hello"; } };
3const child = Object.create(base);
4const a = { x: 1 };
5
6console.log(a.hasOwnProperty("x")); // true
7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
8
9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
hasOwnPropertyเป็นเมธอดสำคัญสำหรับตรวจสอบว่าพร็อพเพอร์ตี้นั้นเป็นของอ็อบเจ็กต์เองหรือไม่isPrototypeOfตรวจสอบว่าอ็อบเจ็กต์เป้าหมายมีตัวมันเองเป็นโปรโตไทป์หรือไม่
Object.keys, Object.values, Object.entries
Object.keys, Object.values, และ Object.entries จะคืนค่ารายชื่อพร็อพเพอร์ตี้ของอ็อบเจ็กต์ที่สามารถวนลูปได้ เครื่องมือเหล่านี้มีประโยชน์สำหรับการวนลูปและแปลงข้อมูล
1// Keys, values and entries
2const item = { id: 1, name: "Widget", price: 9.99 };
3
4// ["id", "name", "price"]
5console.log(Object.keys(item));
6
7// [1, "Widget", 9.99]
8console.log(Object.values(item));
9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));- เครื่องมือเหล่านี้ถูกใช้บ่อยเพื่อนำไปวนลูปและแปลงอ็อบเจ็กต์
Object.assign
Object.assign ใช้สำหรับการคัดลอกและรวมอ็อบเจ็กต์แบบตื้น (shallow copy) โปรดทราบว่า prototype และ accessor property จะไม่ถูกคัดลอก
1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
- สำหรับอ็อบเจ็กต์ซ้อนกัน (nested) จะถูกคัดลอกแค่ reference ดังนั้นควรใช้วิธีอื่นหากต้องการ clone แบบลึก (deep clone)
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze, Object.seal, และ Object.preventExtensions ใช้ควบคุมการเปลี่ยนแปลงของอ็อบเจ็กต์
1// Freeze vs seal vs preventExtensions
2const obj = { a: 1 };
3Object.freeze(obj);
4obj.a = 2; // fails silently or throws in strict mode
5delete obj.a; // fails
6
7const obj2 = { b: 2 };
8Object.seal(obj2);
9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
freezeเข้มงวดที่สุด; จะป้องกันการเปลี่ยนแปลงทุกอย่างกับพร็อพเพอร์ตี้sealป้องกันการเพิ่มหรือการลบคุณสมบัติ แต่ยังสามารถเปลี่ยนค่าของคุณสมบัติที่มีอยู่ได้preventExtensionsจะป้องกันเฉพาะการเพิ่มคุณสมบัติใหม่เท่านั้น คุณสมบัติที่มีอยู่ยังสามารถเปลี่ยนแปลงหรือลบได้
การวนลูปอ็อบเจ็กต์ การจัดลำดับ และ for...in / for...of
for...in จะวนลูปเฉพาะชื่อพร็อพเพอร์ตี้ที่วนลูปได้ รวมถึงของ prototype chain ด้วย จึงมักใช้ร่วมกับ hasOwnProperty การใช้ Object.keys() ร่วมกับ for...of มีความปลอดภัยมากกว่าและทำให้เจตนาของคุณชัดเจนยิ่งขึ้น
1// Safe enumeration
2const obj = Object.create({ inherited: true });
3obj.own = 1;
4
5for (const key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 console.log("own prop:", key);
8 } else {
9 console.log("inherited prop:", key);
10 }
11}
12
13for (const key of Object.keys(obj)) {
14 console.log("key via Object.keys:", key);
15}- กฎของการวนลูปพร็อพเพอร์ตี้ถูกกำหนดไว้ในสเปก ECMAScript บางกรณีลำดับถูกรับประกันได้ บางกรณีไม่สามารถรับประกันได้ โดยหลักทั่วไป คีย์ที่ถูกตีความว่าเป็นตัวเลขจะถูกจัดเรียงตามลำดับจากน้อยไปมาก ส่วนคีย์อื่นจะเรียงตามลำดับที่เพิ่มเข้าไป
การโคลนและคัดลอกอ็อบเจ็กต์แบบลึก (deep copy)
มีวิธีการคัดลอกออบเจกต์อยู่สองประเภท: การคัดลอกแบบตื้นโดยใช้ Object.assign หรือสเปรดซินแทกซ์ และการคัดลอกแบบลึกโดยใช้วิธีการทำซ้ำ ควรเลือกใช้วิธีที่เหมาะสมตามสถานการณ์
คัดลอกแบบตื้น (spread syntax / Object.assign)
1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
- การคัดลอกแบบตื้น อ็อบเจ็กต์ที่ซ้อนกันจะใช้ reference ร่วมกัน ดังนั้นการแก้ไขในตัวต้นฉบับอาจส่งผลถึงตัวที่คัดลอก
คัดลอกแบบลึกอย่างง่าย (มีข้อควรระวัง)
การใช้เทคนิค JSON เป็นวิธีทำ deep copy อย่างรวดเร็ว แต่จะเสียข้อมูลบางอย่างไป เช่น ฟังก์ชัน Date การอ้างอิงแบบวน (circular references) และค่า undefined ถ้าต้องการ deep clone ที่แท้จริง ควรใช้ไลบรารีเฉพาะทาง
1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
- เมธอดที่ใช้ JSON มีความสะดวกสำหรับจัดการข้อมูลง่ายๆ อย่างรวดเร็ว แต่ในหลายกรณี อาจเกิดข้อผิดพลาดจากพฤติกรรมของเมธอดเหล่านี้
Mixin และการประกอบอ็อบเจ็กต์ (object composition)
แทนการสืบทอดหลายชั้น มักเลือกใช้รูปแบบการประกอบพฤติกรรม (behavior) ด้วย mixin
1// Simple mixin function
2const canEat = {
3 eat() { return `${this.name} eats`; }
4};
5const canWalk = {
6 walk() { return `${this.name} walks`; }
7};
8
9function createPerson(name) {
10 const person = { name };
11 return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
- Mixins มีความยืดหยุ่น แต่ต้องระวังชื่อพร็อพเพอร์ตี้ซ้ำ และความสะดวกในการทดสอบ
ข้อผิดพลาดที่พบได้บ่อย และแนวทางที่ดีในการใช้งาน
นี่คือข้อผิดพลาดที่พบบ่อยและแนวทางการใช้งานที่แนะนำ
-
การเปลี่ยนแปลงค่าได้ (mutability) อ็อบเจ็กต์จะเปลี่ยนแปลงค่าได้โดยปกติ ในแอปพลิเคชันที่มีการจัดการสถานะ ควรพิจารณาใช้โครงสร้างข้อมูลแบบไม่เปลี่ยนแปลงโดยใช้
Object.freezeหรือไลบรารีแบบไม่เปลี่ยนแปลง -
โปรโตไทป์ โพลูชัน (prototype pollution) การรวมข้อมูลจากภายนอกเข้าอ็อบเจ็กต์โดยตรงด้วย
Object.assignหรือการวนลูป อาจก่อให้เกิดผลข้างเคียงไม่คาดคิดกับพร็อพเพอร์ตี้พิเศษ เช่น__proto__หรือconstructorซึ่งมีความเสี่ยงด้านความปลอดภัย กรองอินพุตจากผู้ใช้ก่อนนำมารวมกันโดยตรง -
ข้อผิดพลาดของ
for...infor...inจะวนลูปรวมถึงพร็อพเพอร์ตี้ของ prototype ด้วย ควรใช้hasOwnPropertyตรวจสอบ ใช้Object.keysจะเข้าใจง่ายกว่า -
การใช้การคัดลอกแบบตื้นผิดวิธี พิจารณาว่าจำเป็นต้องทำ deep copy หรือไม่ เพื่อป้องกันการเปลี่ยนแปลงในอ็อบเจ็กต์ซ้อนกันกระทบถึงต้นฉบับ
ตัวอย่างการใช้งานจริง: รูปแบบการอัปเดตอ็อบเจ็กต์แบบ immutable
รูปแบบที่คืนค่าอ็อบเจ็กต์ใหม่โดยไม่เปลี่ยนแปลงสถานะโดยตรง นิยมใช้ใน React และไลบรารีที่คล้ายกัน
1// Immutable update example
2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
3
4// Toggle todo done immutably
5function toggleTodo(state, todoId) {
6 return {
7 ...state,
8 todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
9 };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
- โค้ดนี้เป็นตัวอย่างของการสร้างอ็อบเจ็กต์ state ใหม่โดยไม่เปลี่ยนแปลงอ็อบเจ็กต์
stateเดิมโดยตรง ฟังก์ชันtoggleTodoคัดลอกอาเรย์todosและส่งคืนอ็อบเจ็กต์ใหม่โดยที่มีเฉพาะองค์ประกอบเป้าหมายเท่านั้นที่ถูกเปลี่ยนแปลง ดังนั้นstateต้นฉบับจะไม่เปลี่ยนแปลง - การอัปเดตแบบ immutable ลดผลข้างเคียงและช่วยให้จัดการสถานะง่ายขึ้น
ตัวอย่างการใช้งานจริง: การรวมอ็อบเจ็กต์อย่างปลอดภัย (ระวังเรื่อง prototype pollution)
ขณะแปลง JSON ภายนอก ควรละเว้น __proto__ เพื่อป้องกัน prototype pollution
1// Safe merge ignoring __proto__ keys
2function safeMerge(target, source) {
3 for (const key of Object.keys(source)) {
4 if (key === "__proto__" || key === "constructor") continue;
5 target[key] = source[key];
6 }
7 return target;
8}
9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
- การป้องกันเช่นนี้มีความสำคัญกับไลบรารีและเฟรมเวิร์กด้วย
ข้อควรคำนึงด้านประสิทธิภาพ
เกี่ยวกับประสิทธิภาพ ควรพิจารณาประเด็นเหล่านี้:
- หลีกเลี่ยงการเปลี่ยน prototype บ่อย (
Object.setPrototypeOf) หรือเพิ่ม/ลบพร็อพเพอร์ตี้แบบไดนามิก เพราะทำให้โปรแกรมแปลผลทำงานช้าลง - เมื่อสร้างออบเจกต์ขนาดเล็กจำนวนมาก การเพิ่มประสิทธิภาพจะได้ผลดีขึ้นหากใช้ออบเจกต์ที่มีโครงสร้างสม่ำเสมอ (ชุดคุณสมบัติเดียวกัน)
- การคัดลอกแบบลึก (deep copy) ใช้ทรัพยากรมาก ควรลดการใช้งาน หรือพิจารณาใช้อัปเดตแบบเปรียบเทียบความแตกต่าง (diff-based)
สรุป
Object เป็นศูนย์กลางสำคัญของ JavaScript ให้ฟีเจอร์ต่างๆ เช่น การสร้างอ็อบเจ็กต์ ควบคุมพร็อพเพอร์ตี้ การสืบทอด การคัดลอก และการจัดการการเปลี่ยนแปลงค่า การเข้าใจ API เช่น Object.defineProperty Object.assign และ Object.freeze เป็นสิ่งสำคัญ รวมถึงการออกแบบอย่างระมัดระวังเพื่อหลีกเลี่ยงกับดัก เช่น การปนเปื้อนของโปรโตไทป์และการคัดลอกแบบตื้น
คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย