Choosing a data structure
Choose between arrays, objects, Maps, Sets, and WeakMaps by comparing lookup, order, keys, duplicates, grouping, and performance trade-offs.
- 01Separate order from lookupDecide when a numbered list beats a keyed collection, and when it does not.
- 02Pick the right key rulesExplain duplicates, string keys, object keys, and weak object keys.
- 03Group and measure honestlyUse ES2024 grouping and reason about growth without fake timing claims.
Choose the bag for the job
JavaScript gives you several ways to hold data. Arrays, objects, Maps, Sets, and WeakMaps all store values, but they answer different questions. A structure is a promise to future readers: “this data is a list,” “this data is looked up by name,” or “duplicates do not belong here.”
This capstone pulls together Array basics and methods, Object basics, Map & Set, Set operations, WeakMap & WeakSet, and JSON. You do not need formal Big-O yet. We will say it plainly: how much extra work happens as your data grows?
Start with the question your code asks most often. “What is the next item?” points to an array. “Do we have this value?” points to a Set. “What value belongs to this key?” points to an object, Map, or WeakMap.
You pack hiking boots in a backpack, not in a filing cabinet. Data is similar. The right container makes the common action easy and the rules obvious.
- In real life: A suitcase with numbered outfits
- In JavaScript: Array: ordered items, duplicates allowed
- In real life: A phone book by name
- In JavaScript: Object or Map: look up a value by key
- In real life: A guest list at the door
- In JavaScript: Set: membership and uniqueness
- In real life: Sticky notes on borrowed gear
- In JavaScript: WeakMap: metadata attached to objects you do not own
Where the analogy stops: Real bags can hold anything at once. Good programs usually choose one main promise per structure, then convert when a different view is needed.
We will compare lookup vs order, keys and duplicates, honest performance trade-offs, and ES2024 grouping with Object.groupBy and Map.groupBy.
Lookup vs order
STEP THROUGHAn array is a numbered line of people. It is excellent when position matters: first task, second task, last message, sorted score. But if you ask “is Ada in the line?” the array may need to check people one by one until it finds Ada.
A Set is a guest list. It cares about membership and uniqueness. A Map or object is a phone book: give it a key and it jumps to the value for that key. This is not magic; the engine builds lookup machinery because the collection’s job is key access.
If you need the third person in line, a queue is perfect. If you only need to know whether someone is invited, the clipboard wins.
- In real life: A numbered queue
- In JavaScript: Array index and order
- In real life: Walking down the queue looking for a name
- In JavaScript:
array.includes(value) - In real life: Checking a clipboard at the door
- In JavaScript:
set.has(value)
Where the analogy stops: JavaScript engines optimize arrays heavily, and tiny lists are often fine. The analogy is about the growth pattern, not exact timing.
Step through a duplicate-friendly array and a uniqueness-focused Set answering the same lookup question.
script
const blockedSet = new Set(blockedNames);const name = "bot";const arrayAnswer = blockedNames.includes(name);const setAnswer = blockedSet.has(name);console.log(arrayAnswer, setAnswer);Notice that both checks return the same answer. The structure choice is about the rule you mean and the work your program repeats.
Keys & duplicates
STEP THROUGHDuplicates are not automatically bad. A shopping cart can contain two identical mugs. A search history can include the same query twice. Arrays allow that because each entry has its own position. Sets do not: a value is either present or absent.
Keys have rules too. Ordinary object property keys are strings or Symbols. That makes objects perfect for JSON-shaped records such as { theme: "dark" }, but it means the number 1 and the string "1" collide. Map keeps key types and object identity.
Watch ordinary object keys become strings while Map keys keep their original identity.
script
const plain = {};plain[1] = "number one";plain["1"] = "string one";const typed = new Map();typed.set(1, "number one");typed.set("1", "string one");typed.set(objectKey, "object key");console.log(plain[1], typed.size, typed.get(objectKey));| Structure | Best for | Ordered? | Duplicates? | Key types | Lookup | Iterate | JSON-friendly |
|---|---|---|---|---|---|---|---|
| Array | Ordered lists, queues, sorted results | Yes, by index | Yes | Index numbers | Search methods scan unless indexed separately | Excellent | Yes |
| Object | Simple records and string-key dictionaries | Property order has rules; not the main feature | One value per key | String and Symbol property keys | Fast property access by key | Object.keys / entries | Yes |
| Map | Frequently changing dictionaries and non-string keys | Yes, insertion order | One value per key | Any value, including objects | Fast key lookup | Excellent | Needs conversion |
| Set | Unique values and membership tests | Yes, insertion order | No | Values are the keys | Fast membership lookup | Excellent | Needs conversion |
| WeakMap | Private metadata or caches for objects | No | One value per object key | Objects and non-registered Symbols | Fast by object identity | No iteration | No |
JSON stores arrays, objects, strings, numbers, booleans, and null. It does not directly store Map, Set, or WeakMap. Convert them to arrays or plain objects before sending or saving.
Performance trade-offs
INTERACTIVEPerformance starts with growth, not folklore. If a loop calls blocked.includes(user) for every user, the program may scan the blocked list again and again. Turning the blocked list into a Set once lets each membership check ask blockedSet.has(user).
People often write this as O(n) versus O(1): array search work usually grows with the number of items, while Set membership is designed to stay roughly direct. That notation is useful later. For now, read it as “walking the line” versus “checking the clipboard.”
const items = Array.from({ length: 1000 }, (_, i) => "item-" + i);const set = new Set(items);items.includes("item-999"); // may scan many itemsset.has("item-999"); // asks the Set lookup tableNo numbers yet. Press Run benchmark to measure in your browser.
The growth idea matters more than the exact number: an array lookup usually checks values one by one, while a Set lookup is designed to jump straight to a membership answer.
Choose a size, then run it on this device.
Be honest with benchmarks. Timings vary by machine, browser engine, power mode, and surrounding work. The playground runs in your browser only after you click, repeats short runs, and reports a median “on this device.” Tests should prove correctness, not speed.
Object.groupBy & Map.groupBy
STEP THROUGHGrouping turns one ordered list into buckets. ES2024 adds Object.groupBy(items, callback) and Map.groupBy(items, callback). Use Object.groupBy when the group keys are property keys such as strings or Symbols. Use Map.groupBy when keys are objects or other non-string values.
Feature detection in your browser: Checking this browser after the page mounts…
Group an array with ES2024 Object.groupBy and inspect the clean dictionary it returns.
script
{ name: "Notebook", category: "stationery" }, { name: "Pen", category: "stationery" }, { name: "Tea", category: "pantry" },];const byCategory = Object.groupBy(products, item => item.category);const prototype = Object.getPrototypeOf(byCategory);console.log(byCategory.stationery.map(item => item.name).join(", "));console.log(prototype === null);const kitchen = { team: "kitchen" };const office = { team: "office" };const products = [ { name: "Tea", owner: kitchen }, { name: "Notebook", owner: office },];const byOwner = Map.groupBy(products, item => item.owner);console.log(byOwner.get(kitchen).map(item => item.name));Object.groupBy returns a null-prototype object. That is a clean dictionary, but it also means methods from Object.prototype are not sitting on the result. Use Object.hasOwn(result, key) rather than result.hasOwnProperty(key).
Decision guide
INTERACTIVEYou rarely choose from memory. Ask a few questions: does order matter? Do I look up by key? Are duplicates allowed? Are the keys objects? Should object-keyed metadata disappear when the object can be garbage collected? Do I need JSON?
recommendStructure({ ordered: true, keyed: false, duplicates: true, objectKeys: false, weakLifetime: false, json: false,});const tasks = ["plan", "build", "ship"];Recommended: Array. Arrays are the default for ordered lists that can contain duplicates.
- Track unique visitor IDs for this page view
- Render a to-do list in the order the user arranged it
- Read settings such as
themeandlanguageby name and save as JSON - Cache measurements per DOM node without keeping old nodes alive
- Group orders by status strings like
paidandshipped - Process tasks first-in, first-out
- Count how often each word appears
- Keep a unique list of tags while preserving first-seen order
Sort each real scenario by the structure you would reach for first. Some production systems combine structures, but each card has one best first choice.
Where you’ll use this
These choices show up in ordinary app code:
// 1. Ordered UI stateconst tasks = ["write", "review", "ship"]; // 2. JSON-friendly settingsconst settings = { theme: "dark", density: "compact" }; // 3. Counting with a Mapconst counts = new Map();counts.set("bug", (counts.get("bug") ?? 0) + 1); // 4. Fast membership with a Setconst selectedIds = new Set([101, 104, 108]); // 5. Metadata for objects you do not ownconst measurements = new WeakMap();It is normal to convert: receive JSON as objects and arrays, build a Map for repeated lookups, then convert back before saving. The structure nearest the boundary does not have to be the structure in the middle of your algorithm.
Common misconceptions
“Map is always better than object.”
Objects are still ideal for fixed records and JSON. Map shines for dynamic dictionaries, object keys, and frequent entry changes.
“Set sorts or magically cleans data.”
Set removes duplicates using SameValueZero equality and preserves first-seen insertion order. It does not sort values.
“WeakMap is just a faster Map.”
WeakMap is about lifetime. It only accepts object-like keys and cannot be iterated, because entries may disappear when keys are no longer reachable.
“Objects remember every key exactly as written.”
Ordinary object keys become strings unless they are Symbols. Use a Map when number keys, object keys, or separate key types matter.
“Benchmarks tell the whole truth.”
Small timing tests are clues, not laws. First choose the structure that expresses the job; measure real bottlenecks in real code.
Practice: choose and transform
5 EXERCISESA shopping cart must show items in the order added, and duplicate items are allowed. Which structure fits best?
// Best structure for a shopping cart shown in item order, duplicates allowed:
console.log("Array");An Array is best for a beginner cart list because order matters and duplicates can be meaningful.
Read the refactored code. It should keep every user not in the blocked list, including duplicate allowed users.
const users = ["Ada", "Lin", "Grace", "Ada"];
const blocked = ["Lin"];
const blockedSet = new Set(blocked);
console.log(users.filter(user => !blockedSet.has(user)).join(","));const users = ["Ada", "Lin", "Grace", "Ada"];
const blocked = ["Lin"];
const blockedSet = new Set(blocked);
console.log(users.filter(user => !blockedSet.has(user)).join(","));The output stays Ada,Grace,Ada. The Set changes the repeated membership check, not the business rule.
Use Object.groupBy for string status groups. What paid IDs print?
const orders = [
{ id: 1, status: "paid" },
{ id: 2, status: "new" },
{ id: 3, status: "paid" },
];
const byStatus = Object.groupBy(orders, order => order.status);
console.log(byStatus.paid.map(order => order.id).join(","));const orders = [
{ id: 1, status: "paid" },
{ id: 2, status: "new" },
{ id: 3, status: "paid" },
];
const byStatus = Object.groupBy(orders, order => order.status);
console.log(byStatus.paid.map(order => order.id).join(","));Object.groupBy creates a paid bucket and a new bucket. Mapping the paid bucket to IDs prints 1,3.
Trace the loop. What count is stored for the word map?
const words = ["map", "set", "map"];
const counts = new Map();
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
console.log(counts.get("map"));const words = ["map", "set", "map"];
const counts = new Map();
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
console.log(counts.get("map"));A Map is a natural counter dictionary. Each word key stores its current count; map reaches 2.
What unique tag order prints after turning an array into a Set and back?
const tags = ["js", "css", "js", "html", "css"];
console.log([...new Set(tags)].join(","));const tags = ["js", "css", "js", "html", "css"];
console.log([...new Set(tags)].join(","));Set keeps the first js, first css, and html, then spread turns the Set back into an array for joining.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7You need to display messages exactly in arrival order, including duplicates. What should you start with?
Choose an answer to see the explanation.
Question 2 of 7What does this Set size code print?
Read the code, then predictconst ids = [1, 1, 2]; console.log(new Set(ids).size);Choose an answer to see the explanation.
Question 3 of 7What does this object key collision print?
Read the code, then predictconst data = {}; data[1] = "number"; data["1"] = "string"; console.log(data[1]);Choose an answer to see the explanation.
Question 4 of 7Which collection keeps object keys by identity and lets you iterate all entries?
Choose an answer to see the explanation.
Question 5 of 7What does this grouping code print?
Read the code, then predictconst products = [ { type: "book", name: "JS" }, { type: "tool", name: "Pen" }, { type: "book", name: "CSS" }, ]; const grouped = Object.groupBy(products, item => item.type); console.log(grouped.book.length);Choose an answer to see the explanation.
Question 6 of 7When is a timing playground honest?
Choose an answer to see the explanation.
Question 7 of 7What does this Map key code print?
Read the code, then predictconst map = new Map(); map.set(1, "number"); map.set("1", "string"); console.log(map.size);Choose an answer to see the explanation.
Key takeaways
- Arrays are ordered lists. Reach for them when position, sorting, or duplicates matter.
- Objects are JSON-friendly records and simple string-key dictionaries.
- Maps keep key identity and type; Sets keep unique values and fast membership checks.
- WeakMaps attach values to object keys without keeping those keys alive and without iteration.
- Use
Object.groupByfor property-key groups andMap.groupByfor object or non-string keys.
Remember the one-liner.
Choose the structure whose rules match your most common question: order, key lookup, uniqueness, or object lifetime.
Up next: Date & time.