Symbol: Unique Identifiers and Well-Known Symbols in JavaScript
Symbols are unique identifiers that enable advanced metaprogramming. This article covers creating symbols, well-known symbols, and practical applications.
Introduction
Symbols enable:
- Creating unique identifiers
- Hiding properties
- Customizing object behavior
- Implementing protocols
- Advanced metaprogramming
Understanding Symbols helps you:
- Build frameworks
- Create private properties
- Implement custom protocols
- Avoid naming conflicts
Creating Symbols
Basic Symbol Creation
// โ
Good: Create unique symbols
const id = Symbol('id');
const name = Symbol('name');
const email = Symbol('email');
console.log(typeof id); // 'symbol'
console.log(id.toString()); // Symbol(id)
// Each symbol is unique
const id2 = Symbol('id');
console.log(id === id2); // false
Using Symbols as Property Keys
// โ
Good: Use symbols as object keys
const user = {};
const userId = Symbol('userId');
const userEmail = Symbol('userEmail');
user[userId] = 12345;
user[userEmail] = '[email protected]';
console.log(user[userId]); // 12345
console.log(user[userEmail]); // [email protected]
// Symbols don't appear in Object.keys()
console.log(Object.keys(user)); // []
Symbol Registry
// โ
Good: Use global symbol registry
const globalId = Symbol.for('userId');
const globalId2 = Symbol.for('userId');
console.log(globalId === globalId2); // true (same symbol)
// Get symbol key
const key = Symbol.keyFor(globalId);
console.log(key); // 'userId'
Well-Known Symbols
Symbol.iterator
// โ
Good: Implement custom iterator
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next: () => {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}
// Usage
const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
Symbol.asyncIterator
// โ
Good: Implement async iterator
class AsyncRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.asyncIterator]() {
let current = this.start;
const end = this.end;
return {
async next() {
await new Promise(resolve => setTimeout(resolve, 100));
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}
// Usage
async function main() {
const range = new AsyncRange(1, 3);
for await (const num of range) {
console.log(num); // 1, 2, 3 (with delays)
}
}
main();
Symbol.toStringTag
// โ
Good: Customize object string representation
class User {
constructor(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'User';
}
}
const user = new User('John');
console.log(Object.prototype.toString.call(user)); // [object User]
console.log(user.toString()); // [object User]
Symbol.toPrimitive
// โ
Good: Customize primitive conversion
class Money {
constructor(amount) {
this.amount = amount;
}
[Symbol.toPrimitive](/programming/hint) {
if (hint === 'number') {
return this.amount;
}
if (hint === 'string') {
return `$${this.amount}`;
}
return this.amount;
}
}
const money = new Money(100);
console.log(+money); // 100 (number hint)
console.log(`${money}`); // $100 (string hint)
console.log(money + 50); // 150 (default hint)
Symbol.hasInstance
// โ
Good: Customize instanceof behavior
class MyClass {
static [Symbol.hasInstance](/programming/obj) {
return obj.customProperty === true;
}
}
const obj1 = { customProperty: true };
const obj2 = { customProperty: false };
console.log(obj1 instanceof MyClass); // true
console.log(obj2 instanceof MyClass); // false
Symbol.species
// โ
Good: Control derived object type
class MyArray extends Array {
static get [Symbol.species]() {
return Array; // Return Array instead of MyArray
}
}
const arr = new MyArray(1, 2, 3);
const mapped = arr.map(x => x * 2);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
Practical Symbol Patterns
Private Properties
// โ
Good: Create private properties with symbols
const privateData = Symbol('privateData');
class User {
constructor(name, password) {
this.name = name;
this[privateData] = password;
}
verifyPassword(password) {
return this[privateData] === password;
}
}
const user = new User('John', 'secret123');
console.log(user.name); // 'John'
console.log(user[privateData]); // 'secret123' (accessible with symbol)
console.log(Object.keys(user)); // ['name'] (privateData not enumerable)
Metadata Storage
// โ
Good: Store metadata with symbols
const metadata = Symbol('metadata');
function addMetadata(obj, key, value) {
if (!obj[metadata]) {
Object.defineProperty(obj, metadata, {
value: new Map(),
enumerable: false
});
}
obj[metadata].set(key, value);
}
function getMetadata(obj, key) {
return obj[metadata]?.get(key);
}
// Usage
const user = { name: 'John' };
addMetadata(user, 'role', 'admin');
addMetadata(user, 'permissions', ['read', 'write']);
console.log(getMetadata(user, 'role')); // 'admin'
console.log(Object.keys(user)); // ['name'] (metadata hidden)
Event Emitter with Symbols
// โ
Good: Event emitter using symbols
const events = Symbol('events');
class EventEmitter {
constructor() {
this[events] = new Map();
}
on(eventName, handler) {
if (!this[events].has(eventName)) {
this[events].set(eventName, []);
}
this[events].get(eventName).push(handler);
}
emit(eventName, ...args) {
const handlers = this[events].get(eventName);
if (handlers) {
handlers.forEach(handler => handler(...args));
}
}
off(eventName, handler) {
const handlers = this[events].get(eventName);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
}
// Usage
const emitter = new EventEmitter();
emitter.on('message', (msg) => {
console.log('Message:', msg);
});
emitter.emit('message', 'Hello'); // Message: Hello
Mixin Pattern with Symbols
// โ
Good: Mixin pattern using symbols
const canEat = Symbol('canEat');
const canWalk = Symbol('canWalk');
const eater = {
[canEat]() {
return `${this.name} is eating`;
}
};
const walker = {
[canWalk]() {
return `${this.name} is walking`;
}
};
class Animal {
constructor(name) {
this.name = name;
Object.assign(this, eater, walker);
}
}
const dog = new Animal('Rex');
console.log(dog[canEat]()); // 'Rex is eating'
console.log(dog[canWalk]()); // 'Rex is walking'
console.log(Object.keys(dog)); // ['name'] (methods hidden)
Advanced Symbol Patterns
Custom Equality
// โ
Good: Custom equality with symbols
const equals = Symbol('equals');
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
[equals](/programming/other) {
return this.x === other.x && this.y === other.y;
}
}
const p1 = new Point(1, 2);
const p2 = new Point(1, 2);
const p3 = new Point(2, 3);
console.log(p1[equals](/programming/p2)); // true
console.log(p1[equals](/programming/p3)); // false
Custom Serialization
// โ
Good: Custom serialization with symbols
const serialize = Symbol('serialize');
const deserialize = Symbol('deserialize');
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
[serialize]() {
return JSON.stringify({
name: this.name,
email: this.email
});
}
static [deserialize](/programming/json) {
const data = JSON.parse(json);
return new User(data.name, data.email);
}
}
// Usage
const user = new User('John', '[email protected]');
const json = user[serialize]();
const restored = User[deserialize](/programming/json);
console.log(restored.name); // 'John'
console.log(restored.email); // '[email protected]'
Best Practices
-
Use symbols for private data:
// โ Good const privateData = Symbol('privateData'); obj[privateData] = value; -
Use Symbol.for() for global symbols:
// โ Good const globalId = Symbol.for('userId'); -
Implement well-known symbols:
// โ Good [Symbol.iterator]() { // Implementation } -
Hide implementation details:
// โ Good const internal = Symbol('internal'); this[internal] = data;
Common Mistakes
-
Using Symbol() for global symbols:
// โ Bad - creates new symbol each time const id = Symbol('id'); const id2 = Symbol('id'); console.log(id === id2); // false // โ Good - use Symbol.for() const id = Symbol.for('id'); const id2 = Symbol.for('id'); console.log(id === id2); // true -
Forgetting symbols are not enumerable:
// โ Bad - expecting symbol in Object.keys() const sym = Symbol('key'); obj[sym] = value; console.log(Object.keys(obj)); // [] (symbol not included) // โ Good - use Object.getOwnPropertySymbols() console.log(Object.getOwnPropertySymbols(obj)); // [sym] -
Not implementing well-known symbols:
// โ Bad - custom class not iterable class MyClass { // No Symbol.iterator } // โ Good - implement Symbol.iterator class MyClass { [Symbol.iterator]() { // Implementation } }
Summary
Symbols are powerful for metaprogramming. Key takeaways:
- Create unique identifiers with Symbol()
- Use symbols for private properties
- Implement well-known symbols
- Hide implementation details
- Avoid naming conflicts
- Enable custom protocols
- Build advanced abstractions
Related Resources
Next Steps
- Learn about Dynamic Code Evaluation
- Explore Proxy Objects: Interception and Traps
- Study Reflect API
- Practice with symbol patterns
- Build advanced abstractions
Comments