ES6 introduced Symbol โ a primitive type that is always unique โ along with four special collection types: Map, Set, WeakMap, and WeakSet. In ES2021, WeakRef and FinalizationRegistry completed the weak-reference picture. These features solve specific problems that plain objects and arrays cannot: truly unique keys, ordered insertion, iterable key-value pairs with any key type, and memory-efficient caches that do not prevent garbage collection. Understanding these data structures and when to reach for them distinguishes intermediate from senior JavaScript developers.
Symbol
| Feature | Syntax / Behaviour |
|---|---|
| Create | const sym = Symbol('description') โ always unique |
| Global registry | Symbol.for('key') โ same key returns same symbol |
| Retrieve key | Symbol.keyFor(sym) โ returns registry key or undefined |
| As object property | obj[sym] = value โ hidden from for...in and Object.keys |
| Well-known symbols | Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance |
| typeof | 'symbol' |
Map vs Object, Set vs Array
| Feature | Map | Plain Object |
|---|---|---|
| Key types | Any value โ objects, functions, primitives | Strings and Symbols only |
| Key order | Insertion order guaranteed | Guaranteed for strings in modern JS |
| Size | map.size |
Object.keys(obj).length |
| Iterable | Yes โ for...of, spread, destructuring |
No โ need Object.entries |
| Prototype pollution risk | None | Yes โ inherited keys from Object.prototype |
| Performance (many entries) | Better for frequent add/delete | Better for static, known structure |
WeakMap / WeakSet vs Map / Set
| Feature | WeakMap / WeakSet | Map / Set |
|---|---|---|
| Key / value type | Objects only (no primitives) | Any value |
| Holds reference | Weak โ does not prevent GC | Strong โ prevents GC |
| Iterable? | No โ no size, no forEach |
Yes |
| Best for | Private metadata, caches keyed by object | General collections |
| Memory behaviour | Entries removed when key object is GC’d | Entries stay until manually deleted |
Symbol('desc') and Symbol('desc') are NOT equal โ every call creates a brand new unique symbol regardless of the description. The description is only for debugging (it appears in .toString()). Use Symbol.for('key') only when you intentionally want the same symbol across different modules or iframes via the global registry.WeakMap is the perfect tool for associating private metadata with DOM elements or third-party objects without modifying those objects and without preventing garbage collection. When the element is removed from the DOM and all other references are dropped, the WeakMap entry is automatically cleaned up โ no manual bookkeeping required.WeakMap and WeakSet are not iterable and have no size property, you cannot inspect their contents directly. This is intentional โ the non-deterministic nature of garbage collection means the set of live entries is unpredictable. Never use them when you need to list, count, or iterate their contents.Basic Example
// โโ Symbol โ guaranteed uniqueness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false โ always unique
console.log(typeof id1); // 'symbol'
console.log(id1.description); // 'id'
// Symbol as non-enumerable object key
const ID = Symbol('id');
const TYPE = Symbol('type');
const user = {
name: 'Alice',
[ID]: 42,
[TYPE]: 'admin',
};
console.log(user[ID]); // 42
console.log(Object.keys(user)); // ['name'] โ symbols are hidden
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(type)]
// โโ Well-known symbols โ customise language behaviour โโโโโโโโโโโโโโโโโโโโโ
class Temperature {
#celsius;
constructor(c) { this.#celsius = c; }
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.#celsius;
if (hint === 'string') return `${this.#celsius}ยฐC`;
return this.#celsius;
}
static [Symbol.hasInstance](instance) {
return typeof instance?.celsius === 'number';
}
}
const temp = new Temperature(100);
console.log(+temp); // 100 โ numeric hint
console.log(`${temp}`); // '100ยฐC' โ string hint
console.log(temp + 0); // 100 โ default hint
// โโ Map โ any key type, iterable โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cache = new Map();
const keyObj = { id: 1 };
cache.set(keyObj, { data: 'user data', ts: Date.now() });
cache.set('config', { theme: 'dark' });
cache.set(42, 'forty-two');
console.log(cache.get(keyObj)); // { data: 'user data', ts: ... }
console.log(cache.has('config')); // true
console.log(cache.size); // 3
// Iterate
for (const [key, value] of cache) {
console.log(key, '->', value);
}
// Convert to/from object (string keys only)
const obj = Object.fromEntries(
[...cache].filter(([k]) => typeof k === 'string')
);
// โโ Set โ unique values โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const tags = new Set(['js', 'css', 'js', 'html', 'css']);
console.log(tags.size); // 3 โ duplicates removed
console.log([...tags]); // ['js', 'css', 'html']
// Set operations
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union = new Set([...a, ...b]); // {1,2,3,4,5,6}
const intersection = new Set([...a].filter(x => b.has(x))); // {3,4}
const difference = new Set([...a].filter(x => !b.has(x))); // {1,2}
// โโ WeakMap โ private metadata for objects โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const metadata = new WeakMap();
function registerElement(el, data) {
metadata.set(el, { ...data, registeredAt: Date.now() });
}
function getMetadata(el) {
return metadata.get(el);
}
const btn = document.createElement('button');
registerElement(btn, { clicks: 0, label: 'Submit' });
btn.addEventListener('click', () => {
const meta = metadata.get(btn);
meta.clicks++;
console.log(`Clicked ${meta.clicks} times`);
});
// When btn is removed from DOM and all refs dropped:
// The WeakMap entry is automatically garbage collected โ no memory leak
// โโ WeakRef โ observe without preventing GC โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class ResourcePool {
#refs = new Map(); // id -> WeakRef
register(id, resource) {
this.#refs.set(id, new WeakRef(resource));
}
get(id) {
const ref = this.#refs.get(id);
return ref?.deref(); // undefined if GC'd
}
cleanup() {
for (const [id, ref] of this.#refs) {
if (!ref.deref()) this.#refs.delete(id); // remove stale refs
}
}
}
How It Works
Step 1 โ Symbols Prevent Property Name Collisions
When multiple libraries or modules need to add properties to the same object, string keys can clash. Symbols are guaranteed unique โ two libraries using Symbol('id') as a key will never conflict, even if they use the same description string. This makes Symbols the safe way to add extensibility hooks to objects you do not own.
Step 2 โ Map Preserves Insertion Order and Accepts Any Key
Plain objects convert keys to strings โ an object key becomes '[object Object]', losing the reference. Map stores the actual key value by reference. Two different objects are two different Map keys, even if they look identical. This enables caches keyed by DOM elements, request objects, or user instances.
Step 3 โ WeakMap Holds Keys by Weak Reference
A WeakMap holds its key objects weakly โ if no other code holds a reference to the key object, the garbage collector can collect it, and the WeakMap entry disappears automatically. This is fundamentally different from Map, which holds keys strongly and would prevent collection. WeakMap entries are memory-safe by design.
Step 4 โ Well-Known Symbols Hook Into Language Behaviour
Symbol.iterator makes an object work with for...of and spread. Symbol.toPrimitive controls how an object converts to a primitive. Symbol.hasInstance customises instanceof. Symbol.asyncIterator makes objects work with for await...of. These are the extension points the language provides for customising built-in operations.
Step 5 โ WeakRef Enables Caches Without Memory Leaks
new WeakRef(object) holds a weak reference you can check with .deref() โ which returns either the object or undefined if it has been collected. Always check the return value of .deref() before using it. This enables observation patterns and optional caching without preventing the garbage collector from doing its job.
Real-World Example: Private Class State Without Private Fields
// Observable class using WeakMap for private state + Symbol for events
const _state = new WeakMap();
const _handlers = new WeakMap();
class Observable {
constructor(initialState) {
_state.set(this, { ...initialState });
_handlers.set(this, new Map());
}
get(key) {
return _state.get(this)[key];
}
set(key, value) {
const state = _state.get(this);
const prev = state[key];
if (prev === value) return;
state[key] = value;
this.#emit(key, { prev, next: value });
}
on(event, handler) {
const map = _handlers.get(this);
if (!map.has(event)) map.set(event, new Set());
map.get(event).add(handler);
return () => map.get(event)?.delete(handler);
}
#emit(event, detail) {
_handlers.get(this)?.get(event)?.forEach(fn => fn(detail));
}
}
const store = new Observable({ count: 0, theme: 'light' });
const off = store.on('count', ({ prev, next }) => {
console.log(`count: ${prev} -> ${next}`);
});
store.set('count', 1); // count: 0 -> 1
store.set('count', 2); // count: 1 -> 2
off(); // unsubscribe
store.set('count', 3); // (no log โ handler removed)
Common Mistakes
Mistake 1 โ Using Symbol() when Symbol.for() is needed across modules
โ Wrong โ two modules get different symbols:
// module-a.js
const KEY = Symbol('myKey');
export { KEY };
// module-b.js โ a different symbol, even with same description
const KEY = Symbol('myKey');
obj[KEY]; // undefined โ different symbol than module-a's
โ Correct โ use Symbol.for() for shared symbols:
const KEY = Symbol.for('app.myKey'); // same symbol everywhere
Mistake 2 โ Using Map where a plain object is simpler
โ Overkill for static string-keyed data:
const config = new Map([['theme', 'dark'], ['lang', 'en']]);
config.get('theme'); // verbose for simple string key lookups
โ Use Map only when you need object keys, non-string keys, or frequent add/delete:
const config = { theme: 'dark', lang: 'en' }; // plain object is fine here
Mistake 3 โ Not calling .deref() on WeakRef
โ Wrong โ treating WeakRef as the actual object:
const ref = new WeakRef(element);
ref.style.color = 'red'; // TypeError โ ref is a WeakRef, not the element
โ Correct โ always deref and guard against undefined:
const el = ref.deref();
if (el) el.style.color = 'red';
Quick Reference
| Feature | Code |
|---|---|
| Unique symbol | Symbol('desc') |
| Shared symbol | Symbol.for('key') |
| Map โ any key | new Map(); map.set(key, val); map.get(key) |
| Set โ unique values | new Set([1,2,3]); set.has(2) |
| WeakMap โ no leak | new WeakMap(); wm.set(obj, data); wm.get(obj) |
| WeakSet | new WeakSet(); ws.add(obj); ws.has(obj) |
| WeakRef | new WeakRef(obj); ref.deref() ?? fallback |
| Custom iterator | [Symbol.iterator]() { return { next() { ... } } } |