Choosing a data structure
Choose between arrays, objects, Maps, Sets, and WeakMaps by comparing lookup, order, keys, duplicates, grouping, and performance trade-offs.
- 01Tell order apart from findingDecide when a numbered list is the best fit, and when finding a value by its key matters more.
- 02Know the rules for keysExplain how each structure treats repeated values, string keys, object keys, and keys that can go away.
- 03Group a list and think about speedSplit a list into groups with
Object.groupByandMap.groupBy, and think about how work grows without trusting one timing.
Pick the right container for your data
JavaScript gives you several ways to keep a group of values. This lesson compares five of them:
- An array keeps values in a numbered list.
- An object keeps values under names, such as
theme. - A Map keeps values under keys, and a key can be any value.
- A Set keeps each value only once.
- A WeakMap sticks extra data onto objects. The data goes away when its object is no longer used.
Each of these is a data structure: a way to arrange data so that some jobs are easy. Your choice also tells other people what your data is. It says "this is a list", or "look this up by name", or "no repeats here".
This lesson builds on earlier lessons: arrays, objects, Map and Set, Set methods, WeakMap and WeakSet, and JSON. You don't need any math. When we talk about speed, we only ask one question: how much more work does the code do when the data gets bigger?
Start with the question your code asks most often:
- "What is the next item?" Use an array.
- "Do we have this value?" Use a Set.
- "What value goes with this key?" Use an object, a Map, or a WeakMap.
const cart = ["tea", "tea"];
console.log(cart.length, cart[0]);Line 1 makes an array called cart that holds "tea" twice. Line 2 prints the number of items and the first item: 2 tea. The cart keeps both copies, in the order they were added. For this data, order and repeats matter, so an array fits.
You pack hiking boots in a backpack, not in a filing cabinet. Data works the same way. The right container makes your most common job easy. It also makes the rules clear, such as "no repeats here".
- In real life: A suitcase with outfits packed in order
- In JavaScript: Array: values in order, repeats allowed
- In real life: A phone book sorted by name
- In JavaScript: Object or Map: find a value by its key
- In real life: A guest list at the door
- In JavaScript: Set: each value once; is this one in?
- In real life: Sticky notes on gear you borrowed
- In JavaScript: WeakMap: extra data stuck onto objects you don't own
The next sections cover four things:
- Finding a value versus keeping things in order.
- Keys, and values that appear more than once.
- How the work grows as the data grows.
- Splitting a list into groups with
Object.groupByandMap.groupBy.
Finding a value vs keeping order
STEP THROUGHAn array is like a numbered line of people. It is great when position matters: the first task, the last message, or scores sorted from high to low. But say you ask, "Is Ada in the line?" The array may have to check each person, one by one, until it finds Ada.
A Set is like a guest list. It answers one question: is this value in, or not? A Map or an object is like a phone book. You give it a key, and it goes straight to the value for that key. There is no magic here. JavaScript arranges the values inside a Set, a Map, or an object so it can find a key quickly.
const todos = ["buy tea", "buy tea"];
console.log(todos.includes("buy tea"), todos.length);Line 1 makes a to-do list with the same task twice. On line 2, todos.includes("buy tea") checks whether the task is in the list. It gives true. todos.length gives 2, because the list still has both entries. So line 2 prints true 2. If you only cared whether the task is there, a Set would say that more clearly.
If you need the third person in line, the line is perfect. If you only need to know whether someone is invited, the guest list is quicker.
- In real life: A numbered line of people
- In JavaScript: An array, with positions 0, 1, 2, and so on
- In real life: Walking down the line, looking for a name
- In JavaScript:
array.includes(value) - In real life: Checking the guest list at the door
- In JavaScript:
set.has(value)
Step through an array that keeps repeats and a Set that keeps each value once, as both answer the same question.
script
const blockedSet = new Set(blockedNames);const name = "bot";const arrayAnswer = blockedNames.includes(name);const setAnswer = blockedSet.has(name);console.log(arrayAnswer, setAnswer);Both checks give the same answer. So the choice is not about the answer. It is about the rule you mean, and how often your program repeats the check.
Keys and repeated values
STEP THROUGHA duplicate is a value that appears more than once. Duplicates are not always a mistake. A shopping cart can hold two of the same mug. A search history can hold the same search twice. Arrays allow duplicates, because each entry has its own position. Sets do not: a value is either in the Set or not.
"tea" only once.Keys have rules too. A plain object's keys are always strings (text) or Symbols. That makes objects a great fit for JSON-style records such as { theme: "dark" }. But it also means the number 1 and the string "1" become the same key. A Map keeps each key as it is. A number stays a number, and an object key stays that exact object.
const prices = {};
prices[1] = "number key";
prices["1"] = "text key";
console.log(prices[1]);Line 1 makes an empty object. Line 2 stores a value under the number key 1. Line 3 stores a value under the string key "1". Both become the same property name, "1", so line 3 replaces the value from line 2. Line 4 prints text key. If the difference between 1 and "1" matters, use a Map.
Watch a plain object turn its keys into strings, while a Map keeps each key as it is.
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));The table below compares all five structures. Each row is one structure. Each column answers one question about it.
| Structure | What is it best for? | Does it keep order? | Can a value repeat? | What can be a key? | How does it find a value? | Can you loop over it? | Can JSON save it? |
|---|---|---|---|---|---|---|---|
| Array | Lists in order, such as tasks, queues, and sorted results. | Yes, by position: 0, 1, 2, and so on. | Yes. | Position numbers only. | includes checks values one by one. | Yes, easily. | Yes. |
| Object | Records with fixed names, and simple lookups by a string key. | Mostly, but order is not what objects are for. | Each key holds one value. | Strings and Symbols. Other keys turn into strings. | Goes straight to the key. | Yes, with Object.keys or Object.entries. | Yes. |
| Map | Lookups where keys are added and removed often, or keys that aren't strings. | Yes, in the order keys were added. | Each key holds one value. | Any value, even an object. | Goes straight to the key. | Yes, easily. | Not directly. Turn it into an array first. |
| Set | Values that must not repeat, and checking whether a value is in. | Yes, in the order values were added. | No. Each value is kept once. | The values themselves. | has goes straight to the value. | Yes, easily. | Not directly. Turn it into an array first. |
| WeakMap | Extra data stuck onto objects, such as a cache for page elements. | No. | Each key holds one value. | Only objects (and some Symbols). | Goes straight to the exact object. | No. | No. |
JSON is a text format for saving and sending data. It can hold arrays, objects, strings, numbers, true, false, and null. It can't hold a Map, a Set, or a WeakMap. Turn a Map or a Set into an array or a plain object first. Then save or send it.
How the work grows
INTERACTIVETo think about speed, start with growth: how much more work happens when the data gets bigger? Say a loop checks blocked.includes(user) for every user. Each check may walk through the whole blocked array again. Instead, make a Set from the array once. Then each check can be blockedSet.has(user), which goes straight to the answer.
You may see this written as O(n) and O(1). This way of writing is called Big-O notation:
O(n)means the work grows with the number of items,n. Searching an array is usually like this.O(1)means the work stays about the same, however many items there are. A Set check is built to be like this.
For now, think of it as "walking the line" versus "checking the guest list".
const blocked = ["bot", "spam"];
const blockedSet = new Set(blocked);
console.log(blocked.includes("bot"), blockedSet.has("bot"));Line 1 is the list you start with. Line 2 makes a Set from it. That takes some work, but only once. Line 3 prints true true, because both answer the same question. For one small check, the array alone is simpler. For many checks, reusing the Set saves walking the array again and again.
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 Time it to run the code in your browser.
How the work grows matters more than the exact number. An array check usually looks at the values one by one. A Set check goes straight to the answer.
Pick a size, then time it on this device.
A benchmark is a test that times your code. Be careful with its numbers. Timings change with your computer, your browser, battery saving, and whatever else is running. The playground only runs when you click. It does five short runs and shows the median, which is the middle result. The numbers are true for this device only. That's why the lesson's tests check that the code is correct, not how fast it is.
Group a list with Object.groupBy and Map.groupBy
STEP THROUGHGrouping splits one list into smaller lists, called groups. Each item goes into a group based on a key. JavaScript added two tools for this in 2024 (ES2024):
Object.groupBy(items, callback)gives back a plain object. Use it when each group's key is a string (or a Symbol), such as"paid".Map.groupBy(items, callback)gives back a Map. Use it when the keys are objects, or other values that are not strings.
The callback is a function you pass in. JavaScript calls it once for each item. It returns the key of the group that item belongs in.
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.length);The callback on line 6, order => order.status, returns each order's status. JavaScript makes one array for each status it sees: paid and new. Line 7 reads the paid group and prints 2. Inside each group, the orders keep the order they had in the first list.
paid holds 1 and 3, and new holds 2.Does your browser have these tools? Checking your browser once the page has loaded…
Group an array with Object.groupBy, then look at the plain result it gives back.
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);When the group key is an object, use Map.groupBy. Here, each product's owner is an object.
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));Line 7 groups the products by their owner object. Line 8 asks for the group of the exact kitchen object. It prints an array with one name in it, "Tea".
One more detail about Object.groupBy. Most objects borrow methods, such as hasOwnProperty, from a shared object called Object.prototype. That shared object is the object's prototype. The result of Object.groupBy has no prototype, so it borrows nothing. This keeps it clean: a group named toString can't clash with a built-in method. But it also means result.hasOwnProperty(key) doesn't work. Use Object.hasOwn(result, key) instead.
Choose with a few questions
INTERACTIVEYou don't have to choose from memory. Ask yourself these questions:
- Does the order of the values matter?
- Do I find values by a key?
- Can the same value appear twice?
- Are the keys objects?
- Should the extra data go away when its object is no longer used?
- Do I need to save the data as JSON?
The helper's answer is a first guess, not a rule. You can also use two structures together. For example, a Set can remove duplicates, and then an array can show the values in order. Pick the structure that makes the next job easy. Switch to another structure when the job changes, such as just before you save.
recommendStructure({ ordered: true, keyed: false, duplicates: true, objectKeys: false, weakLifetime: false, json: false,});const tasks = ["plan", "build", "ship"];Try: Array. An array is the usual choice for a list in order that can hold repeats.
- Keep each visitor ID once while the page is open
- Show a to-do list in the order the user arranged it
- Read settings such as
themeandlanguageby name, and save them as JSON - Store a measurement for each page element, without keeping removed elements in memory
- Group orders by status names such as
paidandshipped - Handle tasks in the order they arrived: first in, first out
- Count how often each word appears
- Keep each tag once, in the order it first appeared
Sort each real situation under the structure you would try first. Real apps sometimes use two structures together, but each card here has one best first choice.
Where you'll use this
These five choices show up in everyday 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();- Line 2: an array keeps tasks in the order the page shows them.
- Line 5: an object holds settings that you save as JSON.
- Line 9: a Map counts things.
counts.get("bug") ?? 0gives0the first time, because the key isn't there yet. - Line 12: a Set holds the IDs a user has selected, so you can check them quickly.
- Line 15: a WeakMap sticks measurements onto objects that other code made.
It is normal to switch between structures. For example, you get JSON as objects and arrays. You build a Map from it, so repeated lookups are quick. Then you turn it back into an array or an object before you save it. The structure you save doesn't have to be the structure you work with.
Common misconceptions
"A Map is always better than an object."
Objects are still the best fit for records with fixed names and for JSON. A Map is better when you add and remove keys often, or when the keys are objects.
"A Set sorts or cleans the data."
A Set only removes duplicates. It keeps values in the order they were first added. It does not sort them.
"A WeakMap is just a faster Map."
A WeakMap is about how long data lives, not speed. Its keys must be objects. You can't loop over it, because an entry can vanish once nothing else uses its key object.
"Objects keep every key exactly as written."
A plain object turns every key into a string, unless the key is a Symbol. Use a Map when number keys, object keys, or the type of the key matter.
"A benchmark tells the whole truth."
A small timing test is a hint, not a rule. First choose the structure that fits the job. Measure speed only where real code is slow.
"Switching structures loses the meaning of the data."
Switching is useful when the next step needs a different shape. A JSON response can become a Map for quick lookups, then become an array again before you save it.
Practice: choose and change structures
5 EXERCISESA shopping cart must show items in the order they were added. The same item can be added twice. Which structure fits best?
// Best structure for a shopping cart shown in item order, duplicates allowed:
console.log("Array");An Array fits best. Order matters, and the same item can appear more than once. A Set would drop the repeated item.
Read the code. It keeps every user who is not blocked, and keeps repeated users too. What does it print?
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(","));It prints Ada,Grace,Ada. Only Lin is blocked, so both Ada entries stay. The Set only changes how each check is done, not which users are kept.
The code uses Object.groupBy to group orders by their status. Which IDs does it print for the paid group?
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 makes a paid group and a new group. The paid group holds orders 1 and 3. map takes their IDs and join prints 1,3.
Follow the loop one word at a time. 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"));Each word is a key, and its value is the count so far. map goes from 0 to 1, then to 2. So the code prints 2.
The code turns an array into a Set and back into an array. In what order do the tags print?
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(","));The Set keeps the first js, the first css, and html. The spread [...] turns the Set back into an array, and join prints js,css,html.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7You need to show messages in the exact order they arrived, including repeats. What should you start with?
Choose an answer to see the explanation.
Question 2 of 7What size does this Set have?
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 7An object gets the keys 1 and "1". What does this 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 structure lets you use an object as a key, and also loop over all its entries?
Choose an answer to see the explanation.
Question 5 of 7How many items are in the book group?
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 7Which timing playground gives you honest numbers?
Choose an answer to see the explanation.
Question 7 of 7A Map gets the keys 1 and "1". What size does it 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
- An array is a list in order. Use it when position, sorting, or repeated values matter.
- An object is a record with string keys. It is easy to save as JSON.
- A Map keeps every key as it is, even objects. A Set keeps each value once and checks for a value quickly.
- A WeakMap sticks data onto objects. It doesn't keep those objects alive, and you can't loop over it.
- Use
Object.groupBywhen group keys are strings, andMap.groupBywhen they are objects or other values.
Remember the one-liner.
Choose the structure that best answers your most common question: what comes next, what goes with this key, is this value in, or how long should this data live.
Up next: Date & time.