Symbol and Well-Known Symbols
Using Symbols for unique identifiers and metaprogramming
Challenge
Section titled “Challenge”You need to create unique, non-enumerable property keys that won’t conflict with other properties. You also need to customize how objects behave with built-in operations like iteration.
Explanation
Section titled “Explanation”Symbols are primitive values that are unique and non-enumerable. Well-known symbols like Symbol.iterator customize built-in JavaScript behavior.
Symbol Basics
Section titled “Symbol Basics”// Create unique symbolsconst id = Symbol('id');const name = Symbol('name');
// Each symbol is uniqueconsole.log(id === id); // trueconsole.log(Symbol('id') === Symbol('id')); // false
// Use as object keysconst obj = { [id]: 123, [name]: 'Alice'};
console.log(obj[id]); // 123
// Symbols are hidden from regular iterationfor (const key in obj) { console.log(key); // Nothing (symbols not enumerated)}
// But accessible with Object.getOwnPropertySymbolsconsole.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id), Symbol(name)]Solution: Private Properties with Symbols
Section titled “Solution: Private Properties with Symbols”const _password = Symbol('password');const _salt = Symbol('salt');
class User { constructor(username, password) { this.username = username; this[_password] = password; this[_salt] = Math.random(); }
verifyPassword(guess) { return this[_password] === guess; }
// Private methods static #authenticate(user, pwd) { return user[_password] === pwd; }}
const user = new User('john', 'secret123');console.log(user.username); // 'john'console.log(user[_password]); // 'secret123'
// Attempt to find passwordconsole.log(Object.keys(user)); // ['username']console.log(Object.getOwnPropertySymbols(user)); // [Symbol(password), Symbol(salt)]Well-Known Symbols
Section titled “Well-Known Symbols”// Symbol.iterator - make object iterableconst range = { start: 1, end: 5,
[Symbol.iterator]() { let current = this.start; return { next: () => { if (current <= this.end) { return { value: current++, done: false }; } return { done: true }; } }; }};
for (const num of range) { console.log(num); // 1, 2, 3, 4, 5}
// Symbol.toStringTag - customize Object.prototype.toStringclass MyClass { get [Symbol.toStringTag]() { return 'MyClass'; }}
console.log(Object.prototype.toString.call(new MyClass()));// [object MyClass]
// Symbol.hasInstance - customize instanceofclass MyArray { static [Symbol.hasInstance](obj) { return Array.isArray(obj); }}
console.log([] instanceof MyArray); // true
// Symbol.toPrimitive - customize type coercionclass Money { constructor(amount) { this.amount = amount; }
[Symbol.toPrimitive](hint) { if (hint === 'number') { return this.amount; } return `$${this.amount}`; }}
const money = new Money(100);console.log(money + 50); // 150console.log(String(money)); // '$100'Shared Symbols
Section titled “Shared Symbols”// Global symbol registryconst globalSymbol = Symbol.for('mySymbol');const sameSymbol = Symbol.for('mySymbol');
console.log(globalSymbol === sameSymbol); // true
// Retrieve key from symbolconsole.log(Symbol.keyFor(globalSymbol)); // 'mySymbol'
// Useful for cross-realm/cross-module accessPractical Example: Enum Pattern
Section titled “Practical Example: Enum Pattern”const Status = (() => { const _status = Symbol('status');
return Object.freeze({ ACTIVE: { [_status]: 'active' }, INACTIVE: { [_status]: 'inactive' }, PENDING: { [_status]: 'pending' } });})();
// Type-safe enumconst currentStatus = Status.ACTIVE;console.log(currentStatus); // { Symbol(status): 'active' }
// Impossible to accidentally create invalid status// Status.INVALID won't existSymbol Use Cases
Section titled “Symbol Use Cases”// Private dataconst _data = Symbol('data');class Container { constructor(data) { this[_data] = data; }}
// Metadataconst metadata = new Map();metadata.set(Symbol.for('author'), 'John');
// Customizable behaviorclass Logger { [Symbol.for('log')]() { console.log('Logging...'); }}
// Extension pointsconst plugin = { [Symbol.for('initialize')]() { console.log('Plugin initialized'); }};