คลาส `Object` ในภาษา JavaScript

คลาส `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...in for...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 ด้วย

YouTube Video