Skip to content
academia.sh

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.
  • Map does not convert keys, stores them by identity, preserves insertion order, and gives element count directly with size.
  • The value NaN is counted as equal to itself in both Map and Set; objects are compared by identity.
  • JSON.stringify serializes a Map object as an empty object; its content has to be converted to an array of pairs.
  • Set reduces deduplication to a single step; set operations benefit from constant-cost queries.
  • Use Map if 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.

Start typing to search.

↑↓ Esc navigate · open · close