JavaScript Symbol Table
| Piece | What it is | Field note |
|---|---|---|
Symbol(desc) | A unique primitive | Every call is unique even with the same description - never equal to another |
Symbol.for(key) | Registry lookup | SHARED across realms and modules - the one place two symbols can be equal |
Symbol.iterator | The for...of hook | Implement it and for...of, spread and destructuring just work |
Symbol.toPrimitive | Object-to-primitive hook | Controls what +, == and template interpolation coerce your object to |
Symbol.hasInstance | instanceof hook | static [Symbol.hasInstance](obj) decides what instanceof returns |
Symbol.toStringTag | The [object Tag] name | Feeds Object.prototype.toString - the debugging label |
Object.getOwnPropertySymbols | Reflection reads symbols | Symbols are NOT private - JSON.stringify skips them, reflection does not |
enum-like constants | Unique object keys | Color.RED can never collide with a string key or another constant |
Symbols are unique KEYS, not hidden values: they exist so code can attach metadata to objects without colliding with anyone's string properties. Every Symbol('x') call produces a one-of-a-kind primitive - two symbols with identical descriptions are still never equal.
Bottom line: the well-known symbols are the language's official extension points - implement Symbol.iterator and your object works in for...of and spread, implement Symbol.toPrimitive and you control what + and == coerce to, implement Symbol.hasInstance and you decide what instanceof returns. They are how user classes participate in language machinery.
The honest part: symbols are NOT private. Object.getOwnPropertySymbols() reads them by reflection; JSON.stringify merely skips them. The registry is the other surprise - Symbol.for('x') returns the SAME symbol across iframes, workers and modules, which is precisely how cross-realm contracts like the iterator protocol stay coherent.
How to use
- Attach metadata safely: object[Symbol('traceId')] = id - a string property from another library can never collide with it.
- Share a symbol across realms: Symbol.for('myapp.cache') inside an iframe resolves to the same key as the parent page - string keys cannot do that.
- Teach your class the language: get [Symbol.toStringTag]() { return 'Money'; } and every debug print shows [object Money] instead of [object Object].
Frequently asked questions
What is the difference between Symbol('x') and Symbol.for('x')?
Symbol('x') creates a brand-new unique symbol every call - two calls never produce equal values, which is the collision-proof behavior you want for object keys. Symbol.for('x') consults a global registry: first call creates, every later call - in any iframe, worker or module of the same JS engine - returns the SAME symbol. That sharing is the entire point: cross-realm contracts (an object created in one frame being iterated in another) need both sides to reference the identical Symbol.iterator, and only the registry guarantees it.
Are symbol properties private fields?
No - this is the most common misconception. Symbols hide properties from for...in, Object.keys and JSON.stringify, which FEELS like privacy, but Object.getOwnPropertySymbols() and Object.getOwnPropertyDescriptors() enumerate them directly. The reflectable-but-invisible design is intentional: symbols avoid NAME COLLISIONS, not ACCESS. Real privacy in modern JavaScript is the #private class field, which is enforced by the engine - no syntax can reach it from outside the class body.
How do well-known symbols make custom classes work with language features?
They are the hooks the language itself consults. for...of asks the object for Symbol.iterator; spread and destructuring ask the same. The + operator and template interpolation consult Symbol.toPrimitive (before valueOf/toString). instanceof consults Symbol.hasInstance on the RIGHT-hand side - which is why static [Symbol.hasInstance](obj) on a class customizes what 'obj instanceof Class' returns, enabling pattern-like checks (e.g. instanceof ArrayLike). Without these hooks, user classes sit outside language machinery; with them, custom types feel native.
When should I use symbols for enum-like constants?
When uniqueness matters more than serialization. Colors.RED as a symbol can never equal any string or number, so a typo or an accidental numeric mix-up cannot slip through a switch or a map key - and two modules defining the same-named constant still get distinct values. The cost: symbols do not survive JSON, so anything crossing the network, an API boundary or localStorage must convert to strings first. Server-validated enums and persisted options stay strings; in-memory discriminator keys and internal registries are where symbols earn their keep.