JavaScript Symbols
Introduction to Symbols
Symbols are primitive values that represent unique identifiers. They were introduced in ES6 (ES2015) and provide a way to create non-string property keys for objects.
Creating Symbols
Basic Symbol Creation
// Creating a simple Symbol
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false
// Symbol with description
const sym3 = Symbol('mySymbol');
console.log(sym3.description); // 'mySymbol'
// Symbols are always unique
const sym4 = Symbol('mySymbol');
console.log(sym3 === sym4); // false, even with same description
Symbol.for() and Symbol.keyFor()
// Creating shared Symbols using Symbol.for()
const globalSym1 = Symbol.for('globalSymbol');
const globalSym2 = Symbol.for('globalSymbol');
console.log(globalSym1 === globalSym2); // true
// Getting symbol key using Symbol.keyFor()
console.log(Symbol.keyFor(globalSym1)); // 'globalSymbol'
// Regular symbols are not registered
const localSym = Symbol('localSymbol');
console.log(Symbol.keyFor(localSym)); // undefined
Using Symbols as Object Properties
Basic Property Usage
const MY_KEY = Symbol('myKey');
const obj = {
[MY_KEY]: 'Symbol value',
regularKey: 'Regular value'
};
console.log(obj[MY_KEY]); // 'Symbol value'
console.log(Object.keys(obj)); // ['regularKey']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(myKey)]
Symbol Property Enumeration
const obj = {
[Symbol('a')]: 'a',
[Symbol('b')]: 'b',
c: 'c'
};
// Different ways to access properties
console.log(Object.keys(obj)); // ['c']
console.log(Object.getOwnPropertyNames(obj)); // ['c']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b)]
// Getting all properties including symbols
const allProps = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
];
Well-Known Symbols
Symbol.iterator
class CustomCollection {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
// Making object iterable
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.items.length) {
return { value: this.items[index++], done: false };
}
return { done: true };
}
};
}
}
const collection = new CustomCollection();
collection.add('a');
collection.add('b');
for (const item of collection) {
console.log(item); // 'a', 'b'
}
Symbol.toStringTag
class CustomClass {
get [Symbol.toStringTag]() {
return 'CustomClass';
}
}
const obj = new CustomClass();
console.log(Object.prototype.toString.call(obj)); // '[object CustomClass]'
Symbol.toPrimitive
const obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 42;
case 'string':
return 'Custom string';
default:
return 'Default value';
}
}
};
console.log(+obj); // 42
console.log(`${obj}`); // 'Custom string'
console.log(obj + ''); // 'Default value'
Advanced Use Cases
Private Properties (Pre-Class Fields)
const privateProps = new WeakMap();
class PrivateClass {
constructor() {
privateProps.set(this, {
secret: 'private data'
});
}
getSecret() {
return privateProps.get(this).secret;
}
}
const instance = new PrivateClass();
console.log(instance.getSecret()); // 'private data'
console.log(privateProps.get(instance).secret); // 'private data'
Custom Object Types
const TYPE = Symbol('type');
class ValidationError extends Error {
constructor(message) {
super(message);
this[TYPE] = 'ValidationError';
}
get type() {
return this[TYPE];
}
}
const error = new ValidationError('Invalid input');
console.log(error.type); // 'ValidationError'
Registry Pattern
const Registry = {
_registry: new Map(),
register(key, value) {
const sym = Symbol.for(key);
this._registry.set(sym, value);
return sym;
},
get(key) {
return this._registry.get(Symbol.for(key));
}
};
// Usage
Registry.register('config', { env: 'production' });
console.log(Registry.get('config')); // { env: 'production' }
Symbol Metadata and Reflection
Property Descriptors with Symbols
const sym = Symbol('test');
const obj = {};
Object.defineProperty(obj, sym, {
value: 'Symbol value',
writable: true,
enumerable: false,
configurable: true
});
console.log(Object.getOwnPropertyDescriptor(obj, sym));
// {
// value: 'Symbol value',
// writable: true,
// enumerable: false,
// configurable: true
// }
Symbol Property Reflection
const symbols = Object.getOwnPropertySymbols(obj);
const symbolProps = symbols.reduce((acc, sym) => {
acc[sym.description] = obj[sym];
return acc;
}, {});
Best Practices
- Use Descriptive Names
// Good
const RENDER_MODE = Symbol('renderMode');
// Avoid
const s = Symbol();
- Symbol Registry Management
// Centralized symbol management
const AppSymbols = {
events: {
INIT: Symbol('app.events.init'),
READY: Symbol('app.events.ready')
},
config: {
ENV: Symbol('app.config.env'),
MODE: Symbol('app.config.mode')
}
};
- Documentation
/**
* @typedef {Symbol} RenderMode
* Represents the rendering mode of the component
* @property {Symbol} SYNC - Synchronous rendering
* @property {Symbol} ASYNC - Asynchronous rendering
*/
const RenderMode = {
SYNC: Symbol('RenderMode.SYNC'),
ASYNC: Symbol('RenderMode.ASYNC')
};
Symbols provide a powerful way to create unique identifiers and implement special behavior in JavaScript objects. They're particularly useful for:
- Creating non-string property keys
- Implementing well-known behaviors
- Building extensible systems
- Managing private properties
- Creating unique identifiers for registration systems
Remember that Symbols are not private in themselves - they're simply unique identifiers that can be accessed using the appropriate methods.