JAVASCRIPT / MAPS, SETS, SYMBOLS, AND PROXIES
Map fundamentals and keys of any type
Store values under keys of any type in a Map, and predict when two keys count as the same given object identity, NaN, and -0.
What you will learn
- Create, read, update, and delete Map entries with set, get, has, delete, and size
- Use an object or function as a key and look it up by holding onto its reference
- Predict Map key matching for NaN, -0, and two identical-looking object literals
- Iterate a Map in insertion order and spread its keys, values, or entries
Understanding Map fundamentals and keys of any type
A Map is an ordered collection of key/value entries in which the key is kept as the exact value you handed in. A plain object is a property bag, and every property name is a string or a symbol, so obj[1], obj['1'] and obj[true] all end up as the names "1", "1" and "true". Map skips that conversion entirely: a number stays a number, an object stays that one object, a function stays that one function.
Whether two keys count as the same is decided by SameValueZero, which behaves like === with two amendments: NaN matches NaN, and +0 matches -0. For objects, arrays and functions this means reference identity, so { id: 1 } and { id: 1 } are two different keys because Map compares references, not contents. That is why the object you passed to set must be the same object you later pass to get; if you do not keep it in a variable, that entry becomes unreachable even though it still counts toward size.
The surface API is small: size rather than length, plus set, get, has, delete and clear, and set returns the Map so calls chain. Iteration follows insertion order and yields [key, value] arrays, which is why for...of with array destructuring reads so naturally; updating an existing key keeps its original position, while delete followed by set moves it to the end. Note that get returns undefined both for a key that is absent and for a key deliberately stored with the value undefined, so reach for has whenever that difference matters.
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
const seats = new Map();
seats.set(alice, 'A1').set(bob, 'A2').set(NaN, 'unassigned').set(0, 'row 0');
console.log(seats.size);
console.log(seats.get(alice));
console.log(seats.get({ name: 'Alice' }));
console.log(seats.get(NaN));
console.log(seats.get(-0));
for (const [key, value] of seats) {
const label = typeof key === 'object' ? key.name : String(key);
console.log(label + ' -> ' + value);
}A Map stores each key as the exact value you passed in and matches keys by SameValueZero identity instead of converting them to string property names.
Worked examples
Numbers and their string forms stay separate
Shows the key coercion a plain object performs and that a Map performs none.
const obj = {};
obj[1] = 'number one';
obj['1'] = 'string one';
console.log(Object.keys(obj).length, obj[1]);
const map = new Map();
map.set(1, 'number one');
map.set('1', 'string one');
console.log(map.size, map.get(1), map.get('1'));Example explained
Line 1obj[1] and obj['1'] both address the property named "1", so the second assignment overwrites the first.
Line 2Object.keys(obj).length is 1 because only one property name was ever created.
Line 3map.set(1, ...) stores the number itself, so it cannot collide with the string '1' and size becomes 2.
Line 4map.get(1) and map.get('1') return different values because SameValueZero never equates a number with a string.
Insertion order and in-place updates
Demonstrates that overwriting a key keeps its position while deleting and re-adding moves it to the end.
const order = new Map([['a', 1], ['b', 2], ['c', 3]]);
console.log([...order.keys()].join(''));
order.set('a', 99);
console.log([...order.keys()].join(''), order.get('a'));
order.delete('b');
order.set('b', 2);
console.log([...order.keys()].join(''));Example explained
Line 1new Map([[k, v], ...]) inserts entries in array order, so the starting key order is a, b, c.
Line 2order.set('a', 99) matches the existing 'a' entry and replaces only its value, leaving it first.
Line 3order.delete('b') removes the entry outright, so the following set('b', 2) is a fresh insertion appended at the end.
Line 4[...order.keys()] always walks the current insertion order, which is why the last line reads acb.
No prototype keys to trip over
Compares how an object and a Map handle the names toString and __proto__ as keys.
const bad = {};
console.log(typeof bad['toString']);
bad['__proto__'] = 'oops';
console.log(Object.keys(bad).length);
const good = new Map();
console.log(good.has('toString'));
good.set('__proto__', 'fine');
console.log(good.size, good.get('__proto__'));Example explained
Line 1bad['toString'] is a function because the lookup climbs to Object.prototype even though the object is empty.
Line 2Assigning a string to '__proto__' runs the inherited setter, which ignores non-object values, so no own property appears and Object.keys stays empty.
Line 3good.has('toString') is false: a Map's entries are its own data and are unrelated to any prototype chain.
Line 4good.set('__proto__', 'fine') creates an ordinary entry, so size is 1 and get hands the string straight back.
Important notes
It is size, not length. map.length is undefined, so a guard written as if (map.length) never runs and no error is thrown to tell you why.
A Map holds a strong reference to each key, so an object used as a key survives as long as the Map does, and JSON.stringify(map) returns {} because entries are not own properties; convert with Object.fromEntries(map) or [...map] first.
Common mistakes
Looking up an object key with a fresh literal, as in map.get({ id: 1 }), when the entry was set with a different literal: the reference differs, so you get undefined while the entry stays in the Map and still counts toward size.
Writing map[key] = value or reading map[key] instead of using set and get: that creates an ordinary property on the Map object, so size stays 0 and has, get and iteration never see it.
Treating map.get(key) === undefined as proof the key is missing: an entry stored with the value undefined reports as absent, and code that then rebuilds or refetches it will overwrite real data.
Try it yourself
Change, predict, then run
In a browser console, build a Map whose keys are two separately written { id: 1 } literals plus the number 7, the string '7', and NaN, and write down your predicted map.size before running it. Then log map.get(NaN) and map.get({ id: 1 }) and explain each result.
Open the JavaScript workspaceCheck your understanding
Given const k = { id: 1 }; const m = new Map([[k, 'a'], [{ id: 1 }, 'b'], [NaN, 'c'], [NaN, 'd']]); how many entries does m hold and what does m.get(NaN) return?
- 4 entries, and get(NaN) returns 'd'
- 3 entries, and get(NaN) returns 'd'
- 3 entries, and get(NaN) returns undefined
- 2 entries, and get(NaN) returns 'c'
Show answer
The two object literals are separate references, so both survive as distinct keys, but SameValueZero treats NaN as equal to NaN, so the fourth pair overwrites the third instead of adding one: three entries, with 'd' under NaN. The tempting answer of 4 entries comes from assuming Map uses ===, where NaN === NaN is false; Map deliberately departs from === on exactly this point so NaN can be used as a usable key.