060: JavaScript Data Structures: Map, Set, Weak Collections, JSON, and Typed Arrays
Outcomes
By the end of this lesson, you can:
- choose between
Object,Map, andSet; - use
WeakMapandWeakSetappropriately; - explain why weak collections are not enumerable;
- serialize and parse JSON safely;
- identify JSON's data-model limitations;
- explain the role of
ArrayBufferand typed arrays.
Map: Keyed Data with Any Key Type
Start with the design question: are you describing one structured thing, or are you maintaining a collection whose keys are determined dynamically? Objects work very well for records. When the data is fundamentally a changing key/value collection, Map usually expresses that intent more clearly.
const stockByProduct = new Map();
stockByProduct.set("tea", 10);
stockByProduct.set("coffee", 5);
console.log(stockByProduct.get("tea"));
console.log(stockByProduct.has("coffee"));
console.log(stockByProduct.size);
The API makes the common operations explicit: set adds or updates an entry, get retrieves a value, has tests membership, and size reports the number of entries. Unlike object property keys, a Map key can be an object, not only a string or symbol:
const user = { id: 1 };
const preferences = new Map();
preferences.set(user, { theme: "dark" });
console.log(preferences.get(user));
That lookup works because the same object reference is used as the key. A different object with the same properties would be a different key.
Iterating a Map
Map is directly iterable. Each iteration yields a two-item array containing the key and its value, which can be unpacked in the loop:
for (const [key, value] of stockByProduct) {
console.log(key, value);
}
Maps preserve insertion order, so iteration follows the order in which entries were added. That is useful when order is part of how you present or process the collection, but it does not turn a Map into a record with a fixed schema.
Set: Unique Values
Use a Set when the question is whether a value is a member of a collection and each value should occur at most once. Adding a value that is already present does not create another entry:
const selectedTags = new Set();
selectedTags.add("featured");
selectedTags.add("sale");
selectedTags.add("sale");
console.log(selectedTags.size); // 2
The duplicate "sale" is ignored, so size is 2. This makes a Set useful for membership checks and for removing duplicates from an iterable. A compact deduplication pattern is:
const unique = [...new Set(["a", "b", "a"])];
The spread converts the Set back into an ordinary array after the Set has enforced uniqueness.
Object versus Map
Use an object when you are modeling a structured entity with known fields:
const customer = {
id: 1,
name: "Maya",
active: true,
};
Here, id, name, and active describe the customer record. The shape is meaningful to anyone reading the code.
Use a Map when the keys represent dynamic collection data rather than the fixed fields of one entity:
const countsBySku = new Map();
The useful distinction is not that one type has convenient methods and the other does not. It is whether the data has record semantics or collection semantics. Do not replace every object with a Map merely because Map provides methods such as has, get, and set.
WeakMap
Sometimes metadata belongs to an object only for as long as that object is in use. A regular Map would keep its object keys strongly referenced, which can prevent those objects from being garbage-collected. WeakMap is designed for this lifetime relationship: its keys must be objects, and the collection does not keep those key objects alive by itself.
const metadata = new WeakMap();
let button = document.querySelector("button");
metadata.set(button, {
clicks: 0,
});
console.log(metadata.get(button));
The metadata can be retrieved while the button object is still reachable elsewhere. If nothing else references that object, garbage collection can reclaim it. The WeakMap does not provide enumeration that could expose which keys still exist or make their lifetime observable.
That lack of enumeration is intentional. Garbage collection timing is not a stable collection API, so operations such as listing all keys or reading a reliable size would not have predictable meaning. Typical uses include private metadata and caches whose entries should follow the lifetime of their object keys.
WeakSet
WeakSet applies the same lifetime idea to membership rather than key/value metadata. It stores object membership weakly, so its members must be objects:
const processed = new WeakSet();
function process(order) {
if (processed.has(order)) {
return;
}
processed.add(order);
console.log("processing", order.id);
}
The function uses object identity to ensure that the same order object is processed only once while it remains relevant. A WeakSet is not a general-purpose list: it cannot be enumerated, and it is not the right choice when you need to inspect all members later.
Structured Data with JSON
JSON is often used at an API or storage boundary, but it does not represent the entire JavaScript data model. It supports a smaller set of data types, which is useful for interoperability but creates conversion rules you need to understand.
const payload = {
id: 101,
name: "Tea",
active: true,
tags: ["hot", "drink"],
metadata: null,
};
const json = JSON.stringify(payload);
const parsed = JSON.parse(json);
JSON.stringify serializes the JavaScript value into a JSON string. JSON.parse turns a valid JSON string back into JavaScript data. Parsing is not validation of your application's expected shape, though: external JSON can be syntactically valid while still missing fields or containing values of the wrong type. Validate the parsed result before trusting it.
JSON Limitations
JSON does not directly preserve several JavaScript values and object behaviors:
undefined;- functions;
- Symbols;
- BigInts;
MapandSet;Dateidentity;- prototypes;
- circular references.
For example, object properties whose values are undefined or functions are omitted during serialization:
JSON.stringify({
value: undefined,
fn() {},
}); // "{}"
This is a data-model conversion, not a complete clone of the original object. A Date may be represented as a string, but parsing that string does not automatically restore a Date instance. Prototypes and methods are not carried across the JSON boundary, and a circular object graph cannot be represented by JSON's nested value syntax.
BigInt is different: attempting to stringify one throws rather than silently omitting it:
// JSON.stringify({ id: 1n }); // TypeError
Decide explicitly how values such as BigInts, dates, maps, and sets should be represented before sending or storing them.
JSON Reviver and Replacer
The second argument to JSON.stringify can be a replacer function. It lets you transform values or omit selected properties. Returning undefined for a property removes that property from the serialized output:
const json = JSON.stringify(
{ total: 100, secret: "remove-me" },
(key, value) => key === "secret" ? undefined : value
);
The second argument to JSON.parse can be a reviver. It transforms values as the parsed structure is walked, which is useful when a known wire representation needs to become a richer JavaScript value:
const data = JSON.parse(
'{"createdAt":"2026-08-27T10:00:00.000Z"}',
(key, value) => {
if (key === "createdAt") {
return new Date(value);
}
return value;
}
);
These hooks are powerful, but they also hide conversion rules inside serialization and parsing. Use them deliberately; explicit normalization functions are often easier for a team to find, test, and maintain.
Typed Arrays
Ordinary arrays are flexible and can contain mixed JavaScript values. That flexibility is not what binary formats need. Typed arrays provide numeric views over binary memory, with each element constrained to a particular numeric representation.
const bytes = new Uint8Array([65, 66, 67]);
console.log(bytes[0]); // 65
Uint8Array represents unsigned 8-bit values, so the example stores three byte values. The values are numbers rather than strings, even if those byte values happen to correspond to character codes.
Underlying buffer:
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
view[0] = 255;
console.log(view);
ArrayBuffer owns a fixed-length region of raw binary memory. A typed array such as Uint8Array supplies a particular way to view and access that memory. The buffer itself does not decide how its bytes should be interpreted; the view does.
Typed arrays matter when working with:
- files and binary protocols;
- graphics and audio;
- WebAssembly;
- network binary data;
- performance-sensitive numeric work.
Do not use them for ordinary business arrays without a reason. Their value comes from the binary and numeric constraints, not from being a newer replacement for Array.
Worked Example: Inventory Index
This example combines the collection choices. The Map indexes products by SKU for lookup, while the Set records the distinct categories encountered:
function buildInventoryIndex(products) {
const bySku = new Map();
const categories = new Set();
for (const product of products) {
bySku.set(product.sku, product);
categories.add(product.category);
}
return { bySku, categories };
}
const index = buildInventoryIndex([
{ sku: "BIR-1", category: "food", name: "Biryani" },
{ sku: "TEA-1", category: "drink", name: "Tea" },
{ sku: "TEA-2", category: "drink", name: "Black Tea" },
]);
console.log(index.bySku.get("TEA-1"));
console.log([...index.categories]);
The lookup uses the SKU as a dynamic key, while the spread converts the categories Set into an array when an array-shaped result is needed. The repeated "drink" category appears only once in that result.
Failure Example: Stringifying a Map
A Map is not automatically converted into an object containing its entries by JSON. Without an explicit normalization step, its enumerable object properties do not describe the map's contents:
const map = new Map([["tea", 10]]);
console.log(JSON.stringify(map)); // "{}"
Normalize it explicitly when an object representation is appropriate:
const json = JSON.stringify(Object.fromEntries(map));
That representation can be parsed and restored as a Map if the keys and values fit the chosen object representation:
const restored = new Map(
Object.entries(JSON.parse(json))
);
The conversion is a format decision. If the original map uses non-string keys or needs to preserve distinctions that object keys cannot express, choose and document a different JSON representation instead of assuming this round trip is lossless.
Advanced Notes: Complexity and Collection Semantics
Theoretical complexity should inform a collection choice, but it should not be the only consideration. Also account for memory, update patterns, ordering requirements, and whether the data has record or collection semantics.
If you repeatedly search an array by ID:
function findById(items, id) {
return items.find((item) => item.id === id);
}
each lookup scans the array until it finds a match or reaches the end. An index spends work and memory up front so later lookups can use the key directly:
const byId = new Map(
items.map((item) => [item.id, item])
);
console.log(byId.get("P-100"));
That trade-off is valuable when lookups are frequent and the collection changes in controlled ways. If items change, the index must also be updated; otherwise, the faster lookup can return stale data.
Set operations
Modern runtimes increasingly provide Set composition methods, but compatibility varies across environments. A portable intersection can be written directly by filtering one set against membership in the other:
function intersection(first, second) {
return new Set(
[...first].filter((value) => second.has(value))
);
}
The result is a new Set, so neither input is modified.
Binary data and DataView
Typed arrays are useful when one consistent element type is enough. DataView is the lower-level option when a buffer contains fields of different numeric types or when byte order must be specified explicitly. It can read different numeric types and byte orders from an ArrayBuffer:
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint16(0, 500, false);
console.log(view.getUint16(0, false));
The final false selects big-endian byte order for both operations. Matching the format's byte order matters when exchanging binary data with another system.
You do not need binary APIs for most CRUD applications. You should recognize them, however, when working with files, media, hardware protocols, WebSockets, or WebAssembly.
Best Practices
- Use Objects for records and Maps for dynamic keyed collections.
- Use Sets for uniqueness/membership.
- Use weak collections only when object lifetime semantics are part of the problem.
- Treat JSON as a transport/storage representation, not a clone of the JavaScript object model.
- Validate parsed JSON before trusting its shape.
- Use typed arrays only when binary/numeric memory is actually required.
Exercises
Core
Build a Set of unique category names.
Practice
Build a Map keyed by product ID and implement findProduct(id).
Professional Extension
Create serializer/deserializer helpers that convert a Map of inventory quantities to JSON and restore it.
Recap
JavaScript offers several data structures because different data carries different semantics. An object describes a record, a Map manages dynamic keyed entries, a Set models unique membership, weak collections tie auxiliary data to object lifetime, JSON defines a smaller interchange model, and typed arrays expose binary numeric memory. Choosing the structure that matches the data makes both the code's intent and its performance characteristics easier to understand.
