Lesson 12 / 17
Map and Set
The limits of using an object as a dictionary; a mapping type that preserves key identity, sets of unique values, and a comparison of the two setups on grouping.
Contents
The prototype-less object appeared in this course’s first lesson, and the same precaution came up again in later lessons: in the calibration table, in the memoization cache, in the grouping accumulator. The reason was the same every time — in a mapping whose keys come from outside the program, names coming from the object’s chain corrupted the result.
This is not the only limit of using an object as a dictionary. Keys are converted to
strings, the ordering rule is different from what you’d expect, and element count cannot
be read directly. The language offers two types for this job: Map for key–value pairs,
Set for unique values. The lesson first shows the limits, then introduces the two
types.
The Limits of an Object as a Dictionary
An object property’s key can only be a string or a symbol. When a key of another type is given, it is converted to a string.
const dictionary = {}; const keyObject = { sensor: "S-01" }; const otherObject = { sensor: "S-02" }; dictionary[1] = "number one"; dictionary["1"] = "string one"; dictionary[true] = "boolean"; dictionary[keyObject] = "first object"; dictionary[otherObject] = "second object"; console.log(Object.keys(dictionary).join(" | ")); console.log(dictionary[1]); console.log(dictionary[keyObject]); console.log(Object.keys(dictionary).length); const ordered = {}; ordered["S-02"] = 1; ordered[10] = 2; ordered["S-01"] = 3; ordered[2] = 4; console.log(Object.keys(ordered).join(",")); const dirty = { sensor: "S-01" }; console.log("toString" in dirty); console.log(Object.hasOwn(dirty, "toString")); console.log(typeof dirty["toString"]);
1 | true | [object Object] string one second object 3 2,10,S-02,S-01 true false function
Three limits are visible. First, 1 and "1" are the same key; the second assignment
overwrote the first. The second is heavier: two different objects convert to the same
"[object Object]" string, so they collapse into a single key — objects cannot be used
as keys. The third is ordering: keys that look like integers are moved to the front in
ascending numeric order, others keep insertion order.
The last three lines repeat a familiar problem. There is no key named toString in the
dictionary, but the query finds it; whenever keys come from outside, Object.hasOwn or a
prototype-less object has to be used.
A Mapping That Preserves Key Identity
Map does not convert keys. A value of any type can be a key, and comparison is done by
identity.
const keyObject = { sensor: "S-01" }; const otherObject = { sensor: "S-02" }; const ledger = new Map(); ledger.set(1, "number one"); ledger.set("1", "string one"); ledger.set(keyObject, "first object"); ledger.set(otherObject, "second object"); ledger.set(NaN, "undefined measurement"); console.log(ledger.size); console.log(ledger.get(1)); console.log(ledger.get("1")); console.log(ledger.get(keyObject)); console.log(ledger.get({ sensor: "S-01" })); console.log(ledger.get(NaN)); console.log(ledger.has(otherObject)); ledger.delete(otherObject); console.log(ledger.size); const keyTypes = [...ledger.keys()].map((a) => typeof a).join(","); console.log(keyTypes); const ordered = new Map(); ordered.set("S-02", 1); ordered.set(10, 2); ordered.set("S-01", 3); ordered.set(2, 4); console.log([...ordered.keys()].join(","));
5 number one string one first object undefined undefined measurement true 4 number,string,object,number S-02,10,S-01,2
1 and "1" are separate keys. An object key is stored by its identity: querying
with another object of the same content finds nothing. The identity–equality distinction
from the Variables and Binding lesson in the Programming Fundamentals course applies
directly here.
The NaN key being findable points to a detail: comparison is not done with strict
equality, it is done with a rule that counts NaN as equal to itself. The same rule
applies to Set too.
The last line shows the ordering rule: Map preserves only insertion order, it does not
move numeric keys to the front. The size property gives the element count directly; the
object needs all its keys listed for the same information.
Its relationship to hash tables is set up here too. The average constant-cost access
examined in the Hash Tables lesson of the Data Structures course is the behavior
underlying both object properties and the Map structure; Map is its form that
preserves key identity.
Conversions and Traversal
Two-way conversion between Map and an object is built in. In serialization, though, the
difference matters.
const recordObject = { "S-01": 21.4, "S-02": 19.8, "S-03": 18.2 }; const mapFromObject = new Map(Object.entries(recordObject)); console.log(mapFromObject.size); console.log(mapFromObject.get("S-02")); const backToObject = Object.fromEntries(mapFromObject); console.log(JSON.stringify(backToObject)); console.log(JSON.stringify(mapFromObject)); console.log(JSON.stringify([...mapFromObject])); const fromPairs = new Map([ ["S-01", 21.4], ["S-02", 19.8], ]); console.log([...fromPairs.entries()].map(([a, d]) => `${a}=${d}`).join(" ")); let total = 0; for (const [, value] of mapFromObject) total += value; console.log(total.toFixed(1)); mapFromObject.forEach((value, key) => { if (key === "S-01") console.log(`forEach: ${key} -> ${value}`); });
3
19.8
{"S-01":21.4,"S-02":19.8,"S-03":18.2}
{}
[["S-01",21.4],["S-02",19.8],["S-03",18.2]]
S-01=21.4 S-02=19.8
59.4
forEach: S-01 -> 21.4
The fourth line is critical: JSON.stringify serializes a Map object as an empty
object. The reason was established in the first lesson — serialization works with own
enumerable properties, and a Map‘s content is not held as a property. If the data
needs to travel, it has to be converted to an array of pairs. The general solution to the
same problem is taken up in the course’s last lesson.
The for...of loop gives one pair per step; destructuring can separate the key and
value. Because the forEach method’s argument order is reversed — value first, then key
— it is open to being mixed up.
Unique Values
Set is a collection that does not hold the same value more than once. It is the
language’s counterpart to the set introduced in the Sets and Multisets lesson of the Data
Structures course.
const readSensors = ["S-01", "S-02", "S-01", "S-03", "S-02", "S-01"]; const unique = new Set(readSensors); console.log(unique.size); console.log([...unique].join(",")); console.log(unique.has("S-02")); console.log(unique.has("S-99")); const numberSet = new Set([1, 1, NaN, NaN, 0, -0]); console.log(numberSet.size); console.log([...numberSet].join(",")); const objectSet = new Set([{ sensor: "S-01" }, { sensor: "S-01" }]); console.log(objectSet.size); const underMaintenance = new Set(["S-02", "S-04"]); const intersection = [...unique].filter((s) => underMaintenance.has(s)); const difference = [...unique].filter((s) => !underMaintenance.has(s)); const union = [...new Set([...unique, ...underMaintenance])]; console.log(intersection.join(",")); console.log(difference.join(",")); console.log(union.join(","));
3 S-01,S-02,S-03 true false 3 1,NaN,0 2 S-02 S-01,S-03 S-01,S-02,S-03,S-04
Deduplication reduces to a single line, and insertion order is preserved. The numeric
example clarifies the comparison rule: two NaN values are counted as the same — a
behavior that does not hold under strict equality — and, likewise, 0 and -0 are also
counted as the same. Objects are still compared by identity; two objects with the same
content are separate elements.
Set operations benefit from the has query being average constant cost. Doing the same
operation with arrays requires a linear search for every element and pushes the cost up
to the product of the two sets’ sizes.
Comparison in Grouping
The grouping example from the Higher-Order Functions lesson is a suitable test for
placing the two setups side by side. Since sensor names come from a data source, a name
like constructor could well be among them.
const records = [ { sensor: "S-01", value: 21.4 }, { sensor: "S-02", value: 19.8 }, { sensor: "S-01", value: 25.1 }, { sensor: "constructor", value: 0.5 }, ]; function naiveGroup(list) { const group = {}; for (const record of list) { if (!group[record.sensor]) group[record.sensor] = []; group[record.sensor].push(record.value); } return group; } function groupWithObject(list) { const group = {}; for (const record of list) { if (!Array.isArray(group[record.sensor])) group[record.sensor] = []; group[record.sensor].push(record.value); } return group; } function groupWithMap(list) { const group = new Map(); for (const record of list) { if (!group.has(record.sensor)) group.set(record.sensor, []); group.get(record.sensor).push(record.value); } return group; } try { naiveGroup(records); } catch (error) { console.log(`naive version: ${error.constructor.name}`); } const objectGroup = groupWithObject(records); const mapGroup = groupWithMap(records); console.log(Object.keys(objectGroup).join(",")); console.log([...mapGroup.keys()].join(",")); console.log(objectGroup["S-01"].join(",")); console.log(mapGroup.get("S-01").join(",")); console.log(objectGroup["constructor"].join(",")); console.log(mapGroup.get("constructor").join(",")); console.log(Object.keys(objectGroup).length === mapGroup.size);
naive version: TypeError S-01,S-02,constructor S-01,S-02,constructor 21.4,25.1 21.4,25.1 0.5 0.5 true
The naive version throws an error: the expression group["constructor"] finds the
function coming from the chain, this value is counted as true so no array is ever
created, and push is called on the function. The fixed version works because it
validates with a type check — but you have to know the fix is needed.
The version written with Map has no such trap; the has query does not look at the
chain, because Map content is not held as a property. As a rule: use an object if
keys are fixed and written by the programmer, use Map if they come from a data
source.
Summary
- Object keys are converted to strings; different objects fall onto the same key, and keys that look like integers are moved to the front in ordering.
Mapdoes not convert keys, stores them by identity, preserves insertion order, and gives element count directly withsize.- The value
NaNis counted as equal to itself in bothMapandSet; objects are compared by identity. JSON.stringifyserializes aMapobject as an empty object; its content has to be converted to an array of pairs.Setreduces deduplication to a single step; set operations benefit from constant-cost queries.- Use
Mapif keys come from a data source, an object if they are fixed and written in the program.
Next Step
As long as a Map holds a key, that key object stays in memory. Tables set up to attach
extra information to an object therefore extend those objects’ lifetime: every entry not
removed from the table keeps a no-longer-used object alive. The next lesson takes up weak
collections, which do not prevent the objects they hold from being collected, and the
role they play in private data storage.
To keep your progress and take notes, Log in
My notes
Log in to take notes.