ไทย

สำรวจ JavaScript Symbols: วัตถุประสงค์, การสร้าง, การประยุกต์ใช้สำหรับคีย์คุณสมบัติที่ไม่ซ้ำกัน, การจัดเก็บเมทาดาทา และการป้องกันชื่อซ้ำซ้อน พร้อมตัวอย่างการใช้งานจริง

JavaScript Symbols: กุญแจคุณสมบัติ (Property Keys) ที่ไม่ซ้ำกันและเมทาดาทา (Metadata)

JavaScript Symbols ซึ่งเปิดตัวใน ECMAScript 2015 (ES6) เป็นกลไกสำหรับการสร้างคีย์คุณสมบัติที่ไม่ซ้ำกันและไม่สามารถเปลี่ยนแปลงได้ (immutable) ซึ่งแตกต่างจากสตริงหรือตัวเลข Symbols รับประกันได้ว่าจะไม่ซ้ำกันเลยในแอปพลิเคชัน JavaScript ทั้งหมดของคุณ Symbols เป็นวิธีที่ช่วยหลีกเลี่ยงการชนกันของชื่อ (naming collisions) สามารถแนบเมทาดาทาไปกับอ็อบเจกต์ได้โดยไม่รบกวนคุณสมบัติที่มีอยู่ และยังสามารถปรับแต่งพฤติกรรมของอ็อบเจกต์ได้อีกด้วย บทความนี้จะให้ภาพรวมที่ครอบคลุมเกี่ยวกับ JavaScript Symbols ตั้งแต่การสร้าง การใช้งาน ไปจนถึงแนวทางปฏิบัติที่ดีที่สุด

JavaScript Symbols คืออะไร?

Symbol เป็นข้อมูลประเภทพื้นฐาน (primitive data type) ใน JavaScript เช่นเดียวกับตัวเลข, สตริง, บูลีน, null และ undefined อย่างไรก็ตาม สิ่งที่แตกต่างจากข้อมูลประเภทพื้นฐานอื่น ๆ คือ Symbols จะมีค่าที่ไม่ซ้ำกัน ทุกครั้งที่คุณสร้าง Symbol คุณจะได้รับค่าใหม่ที่ไม่ซ้ำกันโดยสิ้นเชิง คุณสมบัติเฉพาะตัวนี้ทำให้ Symbols เหมาะสำหรับ:

การสร้าง Symbols

คุณสามารถสร้าง Symbol ได้โดยใช้คอนสตรัคเตอร์ Symbol() สิ่งสำคัญคือคุณไม่สามารถใช้ new Symbol() ได้ เนื่องจาก Symbols ไม่ใช่อ็อบเจกต์ แต่เป็นค่าพื้นฐาน (primitive values)

การสร้าง Symbol พื้นฐาน

วิธีที่ง่ายที่สุดในการสร้าง Symbol คือ:

const mySymbol = Symbol();
console.log(typeof mySymbol); // Output: symbol

การเรียกใช้ Symbol() ในแต่ละครั้งจะสร้างค่าใหม่ที่ไม่ซ้ำกัน:

const symbol1 = Symbol();
const symbol2 = Symbol();
console.log(symbol1 === symbol2); // Output: false

คำอธิบายของ Symbol

คุณสามารถใส่คำอธิบายที่เป็นสตริง (optional) เมื่อสร้าง Symbol ได้ คำอธิบายนี้มีประโยชน์สำหรับการดีบักและการบันทึกข้อมูล แต่ไม่มีผลต่อความเป็นเอกลักษณ์ของ Symbol

const mySymbol = Symbol("myDescription");
console.log(mySymbol.toString()); // Output: Symbol(myDescription)

คำอธิบายมีไว้เพื่อให้ข้อมูลเท่านั้น Symbols สองตัวที่มีคำอธิบายเดียวกันก็ยังคงเป็นค่าที่ไม่ซ้ำกัน:

const symbolA = Symbol("same description");
const symbolB = Symbol("same description");
console.log(symbolA === symbolB); // Output: false

การใช้ Symbols เป็นคีย์คุณสมบัติ

Symbols มีประโยชน์อย่างยิ่งในการใช้เป็นคีย์คุณสมบัติ เพราะมันรับประกันความเป็นเอกลักษณ์ ซึ่งช่วยป้องกันการชนกันของชื่อเมื่อเพิ่มคุณสมบัติให้กับอ็อบเจกต์

การเพิ่มคุณสมบัติที่เป็น Symbol

คุณสามารถใช้ Symbols เป็นคีย์คุณสมบัติได้เช่นเดียวกับสตริงหรือตัวเลข:

const mySymbol = Symbol("myKey");
const myObject = {};

myObject[mySymbol] = "Hello, Symbol!";

console.log(myObject[mySymbol]); // Output: Hello, Symbol!

การหลีกเลี่ยงการชนกันของชื่อ

ลองจินตนาการว่าคุณกำลังทำงานกับไลบรารีของบุคคลที่สามที่เพิ่มคุณสมบัติลงในอ็อบเจกต์ คุณอาจต้องการเพิ่มคุณสมบัติของคุณเองโดยไม่เสี่ยงที่จะเขียนทับคุณสมบัติที่มีอยู่ Symbols เป็นวิธีที่ปลอดภัยในการทำเช่นนี้:

// ไลบรารีของบุคคลที่สาม (จำลอง)
const libraryObject = {
  name: "Library Object",
  version: "1.0"
};

// โค้ดของคุณ
const mySecretKey = Symbol("mySecret");
libraryObject[mySecretKey] = "Top Secret Information";

console.log(libraryObject.name); // Output: Library Object
console.log(libraryObject[mySecretKey]); // Output: Top Secret Information

ในตัวอย่างนี้ mySecretKey ช่วยให้มั่นใจได้ว่าคุณสมบัติของคุณจะไม่ขัดแย้งกับคุณสมบัติใด ๆ ที่มีอยู่แล้วใน libraryObject

การแจงนับคุณสมบัติที่เป็น Symbol

ลักษณะสำคัญอย่างหนึ่งของคุณสมบัติที่เป็น Symbol คือมันจะถูกซ่อนจากเมธอดการแจงนับมาตรฐาน เช่น for...in loops และ Object.keys() ซึ่งช่วยปกป้องความสมบูรณ์ของอ็อบเจกต์และป้องกันการเข้าถึงหรือแก้ไขคุณสมบัติที่เป็น Symbol โดยไม่ได้ตั้งใจ

const mySymbol = Symbol("myKey");
const myObject = {
  name: "My Object",
  [mySymbol]: "Symbol Value"
};

console.log(Object.keys(myObject)); // Output: ["name"]

for (let key in myObject) {
  console.log(key); // Output: name
}

หากต้องการเข้าถึงคุณสมบัติที่เป็น Symbol คุณต้องใช้ Object.getOwnPropertySymbols() ซึ่งจะคืนค่าอาร์เรย์ของคุณสมบัติที่เป็น Symbol ทั้งหมดในอ็อบเจกต์นั้น:

const mySymbol = Symbol("myKey");
const myObject = {
  name: "My Object",
  [mySymbol]: "Symbol Value"
};

const symbolKeys = Object.getOwnPropertySymbols(myObject);
console.log(symbolKeys); // Output: [Symbol(myKey)]
console.log(myObject[symbolKeys[0]]); // Output: Symbol Value

Well-Known Symbols

JavaScript มีชุดของ Symbols ที่สร้างไว้ล่วงหน้าเรียกว่า Well-Known Symbols ซึ่งเป็นตัวแทนของพฤติกรรมหรือฟังก์ชันการทำงานที่เฉพาะเจาะจง Symbols เหล่านี้เป็นคุณสมบัติของคอนสตรัคเตอร์ Symbol (เช่น Symbol.iterator, Symbol.toStringTag) ซึ่งช่วยให้คุณสามารถปรับแต่งพฤติกรรมของอ็อบเจกต์ในบริบทต่าง ๆ ได้

Symbol.iterator

Symbol.iterator เป็น Symbol ที่กำหนดตัววนซ้ำ (iterator) เริ่มต้นสำหรับอ็อบเจกต์ เมื่ออ็อบเจกต์มีเมธอดที่มีคีย์เป็น Symbol.iterator มันจะกลายเป็นอ็อบเจกต์ที่สามารถวนซ้ำได้ (iterable) ซึ่งหมายความว่าคุณสามารถใช้กับ for...of loops และ spread operator (...) ได้

ตัวอย่าง: การสร้างอ็อบเจกต์ที่สามารถวนซ้ำได้เอง

const myCollection = {
  items: [1, 2, 3, 4, 5],
  [Symbol.iterator]: function* () {
    for (let item of this.items) {
      yield item;
    }
  }
};

for (let item of myCollection) {
  console.log(item); // Output: 1, 2, 3, 4, 5
}

console.log([...myCollection]); // Output: [1, 2, 3, 4, 5]

ในตัวอย่างนี้ myCollection เป็นอ็อบเจกต์ที่ใช้งาน iterator protocol โดยใช้ Symbol.iterator ฟังก์ชัน generator จะส่งคืนค่าแต่ละรายการในอาร์เรย์ items ทำให้ myCollection สามารถวนซ้ำได้

Symbol.toStringTag

Symbol.toStringTag เป็น Symbol ที่ช่วยให้คุณปรับแต่งการแสดงผลเป็นสตริงของอ็อบเจกต์เมื่อมีการเรียกใช้ Object.prototype.toString()

ตัวอย่าง: การปรับแต่งการแสดงผลของ toString()

class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClassInstance';
  }
}

const instance = new MyClass();
console.log(Object.prototype.toString.call(instance)); // Output: [object MyClassInstance]

หากไม่มี Symbol.toStringTag ผลลัพธ์จะเป็น [object Object] Symbol นี้ช่วยให้เราสามารถให้คำอธิบายที่เป็นสตริงที่ชัดเจนยิ่งขึ้นสำหรับอ็อบเจกต์ของเรา

Symbol.hasInstance

Symbol.hasInstance เป็น Symbol ที่ให้คุณปรับแต่งพฤติกรรมของโอเปอเรเตอร์ instanceof โดยปกติแล้ว instanceof จะตรวจสอบว่า prototype chain ของอ็อบเจกต์มีคุณสมบัติ prototype ของคอนสตรัคเตอร์หรือไม่ Symbol.hasInstance ช่วยให้คุณสามารถลบล้างพฤติกรรมนี้ได้

ตัวอย่าง: การปรับแต่งการตรวจสอบ instanceof

class MyClass {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MyClass); // Output: true
console.log({} instanceof MyClass); // Output: false

ในตัวอย่างนี้ เมธอด Symbol.hasInstance จะตรวจสอบว่า instance เป็นอาร์เรย์หรือไม่ ซึ่งทำให้ MyClass ทำหน้าที่เหมือนการตรวจสอบว่าเป็นอาร์เรย์หรือไม่ โดยไม่คำนึงถึง prototype chain ที่แท้จริง

Well-Known Symbols อื่น ๆ

JavaScript ยังมี Well-Known Symbols อื่น ๆ อีกหลายตัว ได้แก่:

Global Symbol Registry

บางครั้งคุณอาจต้องการแชร์ Symbols ระหว่างส่วนต่าง ๆ ของแอปพลิเคชัน หรือแม้กระทั่งระหว่างแอปพลิเคชันต่าง ๆ Global Symbol Registry เป็นกลไกที่ใช้สำหรับลงทะเบียนและดึงข้อมูล Symbols โดยใช้คีย์

Symbol.for(key)

เมธอด Symbol.for(key) จะตรวจสอบว่ามี Symbol ที่มีคีย์ที่กำหนดอยู่ใน global registry หรือไม่ หากมีอยู่ ก็จะคืนค่า Symbol นั้น หากไม่มี ก็จะสร้าง Symbol ใหม่ด้วยคีย์นั้นและลงทะเบียนไว้ใน registry

const globalSymbol1 = Symbol.for("myGlobalSymbol");
const globalSymbol2 = Symbol.for("myGlobalSymbol");

console.log(globalSymbol1 === globalSymbol2); // Output: true
console.log(Symbol.keyFor(globalSymbol1)); // Output: myGlobalSymbol

Symbol.keyFor(symbol)

เมธอด Symbol.keyFor(symbol) จะคืนค่าคีย์ที่เชื่อมโยงกับ Symbol ใน global registry หาก Symbol นั้นไม่ได้อยู่ใน registry ก็จะคืนค่าเป็น undefined

const mySymbol = Symbol("localSymbol");
console.log(Symbol.keyFor(mySymbol)); // Output: undefined

const globalSymbol = Symbol.for("myGlobalSymbol");
console.log(Symbol.keyFor(globalSymbol)); // Output: myGlobalSymbol

สำคัญ: Symbols ที่สร้างด้วย Symbol() จะ*ไม่*ถูกลงทะเบียนใน global registry โดยอัตโนมัติ เฉพาะ Symbols ที่สร้าง (หรือดึงข้อมูล) ด้วย Symbol.for() เท่านั้นที่จะเป็นส่วนหนึ่งของ registry

ตัวอย่างการใช้งานจริงและกรณีศึกษา

นี่คือตัวอย่างการใช้งานจริงที่แสดงให้เห็นว่า Symbols สามารถนำไปใช้ในสถานการณ์จริงได้อย่างไร:

1. การสร้างระบบปลั๊กอิน

Symbols สามารถใช้เพื่อสร้างระบบปลั๊กอินที่โมดูลต่าง ๆ สามารถขยายฟังก์ชันการทำงานของอ็อบเจกต์หลักได้โดยที่คุณสมบัติของแต่ละโมดูลไม่ขัดแย้งกัน

// อ็อบเจกต์หลัก
const coreObject = {
  name: "Core Object",
  version: "1.0"
};

// ปลั๊กอิน 1
const plugin1Key = Symbol("plugin1");
coreObject[plugin1Key] = {
  description: "Plugin 1 adds extra functionality",
  activate: function() {
    console.log("Plugin 1 activated");
  }
};

// ปลั๊กอิน 2
const plugin2Key = Symbol("plugin2");
coreObject[plugin2Key] = {
  author: "Another Developer",
  init: function() {
    console.log("Plugin 2 initialized");
  }
};

// การเข้าถึงปลั๊กอิน
console.log(coreObject[plugin1Key].description); // Output: Plugin 1 adds extra functionality
coreObject[plugin2Key].init(); // Output: Plugin 2 initialized

ในตัวอย่างนี้ ปลั๊กอินแต่ละตัวใช้คีย์ Symbol ที่ไม่ซ้ำกัน ซึ่งช่วยป้องกันการชนกันของชื่อที่อาจเกิดขึ้นและทำให้มั่นใจได้ว่าปลั๊กอินสามารถทำงานร่วมกันได้อย่างราบรื่น

2. การเพิ่มเมทาดาทาให้กับองค์ประกอบ DOM

Symbols สามารถใช้เพื่อแนบเมทาดาทาไปกับองค์ประกอบ DOM ได้โดยไม่รบกวนแอตทริบิวต์หรือคุณสมบัติที่มีอยู่

const element = document.createElement("div");

const dataKey = Symbol("elementData");
element[dataKey] = {
  type: "widget",
  config: {},
  timestamp: Date.now()
};

// การเข้าถึงเมทาดาทา
console.log(element[dataKey].type); // Output: widget

วิธีนี้ช่วยให้เมทาดาทาแยกออกจากแอตทริบิวต์มาตรฐานขององค์ประกอบ ซึ่งช่วยปรับปรุงความสามารถในการบำรุงรักษาและหลีกเลี่ยงความขัดแย้งที่อาจเกิดขึ้นกับ CSS หรือโค้ด JavaScript อื่น ๆ

3. การสร้างคุณสมบัติส่วนตัว (Private Properties)

แม้ว่า JavaScript จะไม่มีคุณสมบัติส่วนตัวที่แท้จริง แต่ Symbols สามารถใช้เพื่อจำลองความเป็นส่วนตัวได้ โดยการใช้ Symbol เป็นคีย์คุณสมบัติ คุณสามารถทำให้โค้ดภายนอกเข้าถึงคุณสมบัตินั้นได้ยาก (แต่ไม่ใช่ว่าเป็นไปไม่ได้)

class MyClass {
  #privateSymbol = Symbol("privateData"); // หมายเหตุ: ไวยากรณ์ '#' นี้เป็นฟิลด์ส่วนตัว *จริง* ที่เปิดตัวใน ES2020 ซึ่งแตกต่างจากตัวอย่าง

  constructor(data) {
    this[this.#privateSymbol] = data;
  }

  getData() {
    return this[this.#privateSymbol];
  }
}

const myInstance = new MyClass("Sensitive Information");
console.log(myInstance.getData()); // Output: Sensitive Information

// การเข้าถึงคุณสมบัติ "ส่วนตัว" (ทำได้ยาก แต่เป็นไปได้)
const symbolKeys = Object.getOwnPropertySymbols(myInstance);
console.log(myInstance[symbolKeys[0]]); // Output: Sensitive Information

แม้ว่า Object.getOwnPropertySymbols() จะยังคงเปิดเผย Symbol ได้ แต่วิธีนี้ทำให้โอกาสที่โค้ดภายนอกจะเข้าถึงหรือแก้ไขคุณสมบัติ "ส่วนตัว" โดยไม่ได้ตั้งใจนั้นน้อยลง หมายเหตุ: ปัจจุบัน JavaScript สมัยใหม่มีฟิลด์ส่วนตัวที่แท้จริง (ใช้คำนำหน้า `#`) ซึ่งให้การรับประกันความเป็นส่วนตัวที่แข็งแกร่งกว่า

แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ Symbols

นี่คือแนวทางปฏิบัติที่ดีที่สุดที่ควรคำนึงถึงเมื่อทำงานกับ Symbols:

สรุป

JavaScript Symbols เป็นกลไกที่มีประสิทธิภาพสำหรับการสร้างคีย์คุณสมบัติที่ไม่ซ้ำกัน การแนบเมทาดาทาไปกับอ็อบเจกต์ และการปรับแต่งพฤติกรรมของอ็อบเจกต์ ด้วยความเข้าใจในวิธีการทำงานของ Symbols และการปฏิบัติตามแนวทางที่ดีที่สุด คุณจะสามารถเขียนโค้ด JavaScript ที่มีความเสถียร บำรุงรักษาง่าย และปราศจากการชนกันของชื่อได้ ไม่ว่าคุณจะสร้างระบบปลั๊กอิน เพิ่มเมทาดาทาให้กับองค์ประกอบ DOM หรือจำลองคุณสมบัติส่วนตัว Symbols ก็เป็นเครื่องมือที่มีค่าสำหรับปรับปรุงเวิร์กโฟลว์การพัฒนา JavaScript ของคุณ