A data structure is a way of storing values so that the operation you need is simple and fast. In JavaScript, an array holds an ordered list, an object holds a record with known fields, a Map is a dictionary with keys of any type, and a Set is a collection of unique values with a fast "is it there" check. The choice starts with one question: how will you look the data up — by position, by field name, by an arbitrary key or by the value itself?
After this you will be able to
- Choose between an array, an object, a Map and a Set based on how the data will be read and changed
- Transform a list with map, filter, find, reduce and toSorted without touching the original array
- Group orders with Object.groupBy or a Map and compute totals per group
- Explain the difference between a shallow copy and structuredClone
Know beforehand
Verified with
Node.js 24.18 LTS (V8 13.6)ECMAScript 2025
Checked against docs: October 11, 2026
Contents
Four structures, four ways to find data
In a real program, data rarely lives alone. Orders arrive as a list, a customer is described by a set of fields, statuses need labels, and product tags must not repeat. JavaScript has a built-in structure for each of these jobs, and a poor choice usually shows up quickly: the code fills up with manual checks and slows down on larger data.
Array
- What it holds
- An ordered list, duplicates allowed
- How you find an element
- By index; by condition — by scanning (
find,includes) - When to use it
- A list to display, sort or walk through in order
Object
- What it holds
- A record: properties with string names
- How you find an element
- By field name:
order.status - When to use it
- A single entity with known fields; data for JSON
Map
- What it holds
- Key–value pairs, keys of any type
- How you find an element
map.get(key)- When to use it
- A dictionary that grows at runtime; object keys
Set
- What it holds
- Unique values
- How you find an element
set.has(value)- When to use it
- Remove duplicates, check whether something is in the collection
Speed depends on the choice too. The specification requires Map and Set to find an element, on average, faster than in time proportional to the number of elements; MDN explains that internally this may be a hash table or a search tree. For Set, MDN states plainly that has is on average faster than Array.prototype.includes on an array of the same length: includes compares the value with the elements one by one. How to reason about such differences is covered in the lesson on algorithms and complexity.
Array: order, indexes and methods
An array is a numbered list. Indexes start at zero, and the last element sits at length − 1. A JavaScript array can grow and shrink, and its elements can be of different types. The at(-1) method reads from the end without arithmetic on the length.
The main methods take a callback — a function the method calls for each element. If arrow functions still feel unfamiliar, revisit the lesson on functions. It matters which methods create a new array and which change the original:
map
- What it returns
- A new array of the same length
- Changes the original array
- No
filter
- What it returns
- A new array with the matching elements
- Changes the original array
- No
find
- What it returns
- The first matching element or
undefined - Changes the original array
- No
reduce
- What it returns
- A single value: a sum, an object, a Map
- Changes the original array
- No
sort
- What it returns
- The same array, now sorted
- Changes the original array
- Yes
toSorted
- What it returns
- A sorted copy
- Changes the original array
- No
const orders = [
{ id: 101, customer: "Aigerim", status: "paid", total: 12500 },
{ id: 102, customer: "Daniyar", status: "new", total: 4800 },
{ id: 103, customer: "Aigerim", status: "shipped", total: 23000 },
{ id: 104, customer: "Maria", status: "paid", total: 7600 },
{ id: 105, customer: "Yerzhan", status: "cancelled", total: 3100 },
];
// Index access: counting starts at zero, at(-1) is the last element
console.log(orders[0].id); // 101
console.log(orders.at(-1).id); // 105
console.log(orders.length); // 5
// map — a new array of the same length
const ids = orders.map((order) => order.id);
console.log(ids); // [ 101, 102, 103, 104, 105 ]
// filter — a new array with the matching elements
const paid = orders.filter((order) => order.status === "paid");
console.log(paid.length); // 2
// find — the first matching element or undefined
const large = orders.find((order) => order.total > 20000);
console.log(large?.customer); // Aigerim
console.log(orders.find((order) => order.total > 100000)); // undefined
// reduce — folds the array into a single value
const revenue = orders
.filter((order) => order.status !== "cancelled")
.reduce((sum, order) => sum + order.total, 0);
console.log(revenue); // 47900
// toSorted — a sorted copy, the original array stays as it is
const byTotal = orders.toSorted((a, b) => b.total - a.total);
console.log(byTotal.map((order) => order.id)); // [ 103, 101, 104, 102, 105 ]
console.log(orders[0].id); // 101
// Without a compare function, numbers are compared as strings
console.log([10, 9, 100, 1].toSorted()); // [ 1, 10, 100, 9 ]
console.log([10, 9, 100, 1].toSorted((a, b) => a - b)); // [ 1, 9, 10, 100 ]node arrays.mjs. The comments on the right are the actual Node.js 24 output.Object: a record with fields
An object is a set of name–value properties. It fits a single entity: an order has a number, a customer, a status and a total. Data from an API usually arrives in exactly this shape as JSON, which makes the object the most familiar record format in web development.
Destructuring is syntax that pulls properties into variables in one line. You can rename a field (total: amount) and set a default value in case the property is missing.
const order = { id: 101, customer: "Aigerim", status: "paid", total: 12500 };
// Reading and writing properties
console.log(order.customer); // Aigerim
order.status = "shipped";
console.log(order["status"]); // shipped
// Destructuring: pull fields into variables, rename, set a default value
const { id, total: amount, comment = "no comment" } = order;
console.log(id, amount, comment); // 101 12500 no comment
// Keys and key–value pairs
console.log(Object.keys(order)); // [ 'id', 'customer', 'status', 'total' ]
console.log(Object.entries(order)[0]); // [ 'id', 101 ]
// An object as a lookup table with keys known in advance
const statusLabels = {
new: "New",
paid: "Paid",
shipped: "Shipped",
cancelled: "Cancelled",
};
console.log(statusLabels[order.status]); // Shipped
// A missing key gives undefined, not an error
console.log(statusLabels.returned); // undefined
console.log(statusLabels.returned ?? "Unknown status"); // Unknown statusnode objects.mjs.An object often works as a lookup table too: status → label for the UI. That is a fine choice while the keys are known in advance, few in number and not supplied by users. When keys appear while the program runs, a Map is the better fit.
Map vs. object: which one when
For a long time the object was JavaScript's only dictionary. Map came later, and MDN lists the differences that make it the more convenient choice in many cases:
Default keys
- Object
- Inherited from the prototype, such as
constructor - Map
- Only the ones you added
Key type
- Object
- String or Symbol
- Map
- Anything: an object, a number, a function
Key order
- Object
- Defined, but by complex rules — MDN advises not to rely on it
- Map
- Insertion order
Size
- Object
Object.keys(obj).length- Map
map.size
Iteration
- Object
- Via
Object.keysorObject.entries - Map
- Directly in
for...of
Frequent additions and removals
- Object
- Not optimized for them
- Map
- Performs better
JSON
- Object
JSON.stringifyout of the box- Map
- Needs converting first
// The dictionary-object trap: keys inherited from the prototype
const stock = { latte: 3 };
console.log("constructor" in stock); // true — inherited from Object.prototype
console.log(Object.hasOwn(stock, "constructor")); // false
const stockMap = new Map([["latte", 3]]);
console.log(stockMap.has("constructor")); // false — a Map holds only what you put in
// A Map key can be an object; a plain object turns the key into a string
const aigerim = { id: 1 };
const daniyar = { id: 2 };
const lastVisit = new Map();
lastVisit.set(aigerim, "2026-10-01").set(daniyar, "2026-10-09");
console.log(lastVisit.get(aigerim)); // 2026-10-01
console.log(lastVisit.size); // 2
const asObject = {};
asObject[aigerim] = "2026-10-01";
asObject[daniyar] = "2026-10-09";
console.log(Object.keys(asObject)); // [ '[object Object]' ] — both keys became the same string
// A counter: frequent additions and updates are where Map shines
const visits = new Map();
for (const name of ["Aigerim", "Daniyar", "Aigerim"]) {
visits.set(name, (visits.get(name) ?? 0) + 1);
}
console.log(visits); // Map(2) { 'Aigerim' => 2, 'Daniyar' => 1 }
// A Map does not serialize to JSON by itself — convert it to an object first
console.log(JSON.stringify(visits)); // {}
console.log(JSON.stringify(Object.fromEntries(visits))); // {"Aigerim":2,"Daniyar":1}node map.mjs. Note the line with [object Object]: the object turned both keys into the same string.A practical rule: an object for a record with fixed fields and for data that will go to JSON; a Map for a dictionary that fills up while the program runs, especially if the keys are not strings or need frequent adding and removing.
Set: uniqueness and the "is it there" check
A Set is a collection in which each value occurs once. Adding a value again changes nothing. Values are compared almost like ===; the exception is NaN, which a Set treats as equal to itself. Objects are compared by reference, so two objects with identical content are two different elements.
const tags = ["coffee", "dessert", "coffee", "breakfast", "dessert"];
// A Set stores each value once and remembers insertion order
const unique = new Set(tags);
console.log(unique.size); // 3
console.log([...unique]); // [ 'coffee', 'dessert', 'breakfast' ]
unique.add("coffee");
console.log(unique.size); // 3 — the duplicate was not added
console.log(unique.has("breakfast")); // true
// Set operations (Node.js 22+, current browsers)
const morning = new Set(["coffee", "pancakes", "porridge"]);
const evening = new Set(["coffee", "pasta"]);
console.log(morning.intersection(evening)); // Set(1) { 'coffee' }
console.log(morning.difference(evening)); // Set(2) { 'pancakes', 'porridge' }
console.log(morning.union(evening).size); // 4
// Objects are compared by reference, not by content
const pair = new Set([{ id: 1 }, { id: 1 }]);
console.log(pair.size); // 2
// Checking "is it on the list"
const blockedIds = new Set([7, 19, 42]);
const incoming = [3, 19, 25, 42];
console.log(incoming.filter((id) => !blockedIds.has(id))); // [ 3, 25 ]node set.mjs. The intersection, union and difference methods are available in Node.js 22 and later.The intersection, union and difference methods do what the matching operations on sets do in mathematics. They used to be written by hand with filter and has; now they are part of the language.
Copying and mutation: spread and structuredClone
Mutation means changing a value that already exists. A variable holding an object or array stores a reference to it, not the thing itself, so two variables can point at the same data. Change it through one, and you see the change through the other.
MDN stresses that all built-in copy operations — spread ..., Array.from, slice, concat — create shallow copies. Only the top level is new; nested objects and arrays stay shared. structuredClone makes a deep copy: it copies nested structures and even circular references, but throws a DataCloneError when it meets something that cannot be cloned, such as a function.
const original = { id: 101, items: [{ sku: "latte", qty: 1 }] };
// Spread copies only the top level
const shallow = { ...original };
shallow.id = 999;
shallow.items[0].qty = 5;
console.log(original.id); // 101 — the top level is independent
console.log(original.items[0].qty); // 5 — the nested array is shared
// structuredClone makes a deep copy
const deep = structuredClone(original);
deep.items[0].qty = 1;
console.log(original.items[0].qty); // 5
console.log(deep.items[0].qty); // 1
// structuredClone cannot copy functions
try {
structuredClone({ format() {} });
} catch (error) {
console.log(error.name); // DataCloneError
}
// sort changes the original array, toSorted does not
const totals = [300, 1000, 25];
const sorted = totals.toSorted((a, b) => a - b);
console.log(totals); // [ 300, 1000, 25 ]
console.log(sorted); // [ 25, 300, 1000 ]
totals.sort((a, b) => a - b);
console.log(totals); // [ 25, 300, 1000 ]node copy.mjs.- A flat object or array with no nesting — spread is enough:
{ ...order },[...items] - Nested data that must change independently —
structuredClone - A sorted or modified version without spoiling the original —
toSorted,toReversed,toSpliced,withinstead ofsort,reverse,splice
Practice: grouping orders by status
Let's put it all together on a typical task from a store's admin panel: there is a list of orders, and we need to split them by status, count the orders and the total in each group, find the customer with the highest revenue and learn how many different customers there are.
Group
Object.groupByputs elements into groups by the key the callback returns. The keys are status strings, so an object works. MDN notes that the result is created without a prototype, so it has no stray inherited keys.Compute totals
Object.entriesturns the groups into pairs,mapbuilds a report row,reducesums the orders inside a group.Collect revenue per customer
Customers appear as we iterate, so we accumulate sums in a Map and skip cancelled orders.
Sort
Spread turns the Map into an array of pairs, and we sort a copy by revenue.
Count the unique ones
A Set of customer names removes the duplicates by itself; the answer is in
size.
const orders = [
{ id: 101, customer: "Aigerim", status: "paid", total: 12500 },
{ id: 102, customer: "Daniyar", status: "new", total: 4800 },
{ id: 103, customer: "Aigerim", status: "shipped", total: 23000 },
{ id: 104, customer: "Maria", status: "paid", total: 7600 },
{ id: 105, customer: "Yerzhan", status: "cancelled", total: 3100 },
{ id: 106, customer: "Daniyar", status: "paid", total: 15400 },
{ id: 107, customer: "Maria", status: "new", total: 2900 },
];
// 1. Group by status: the keys are strings, so Object.groupBy fits
const byStatus = Object.groupBy(orders, (order) => order.status);
console.log(Object.keys(byStatus)); // [ 'paid', 'new', 'shipped', 'cancelled' ]
console.log(byStatus.paid.map((order) => order.id)); // [ 101, 104, 106 ]
// 2. Totals per status: count and sum for each group
const summary = Object.entries(byStatus).map(([status, list]) => ({
status,
count: list.length,
sum: list.reduce((total, order) => total + order.total, 0),
}));
console.log(summary[0]); // { status: 'paid', count: 3, sum: 35500 }
// 3. Revenue per customer, cancelled orders excluded — a Map adds keys on the fly
const byCustomer = new Map();
for (const order of orders) {
if (order.status === "cancelled") continue;
byCustomer.set(order.customer, (byCustomer.get(order.customer) ?? 0) + order.total);
}
console.log(byCustomer.get("Aigerim")); // 35500
// 4. Top customers by revenue: Map → array of pairs → sorted copy
const top = [...byCustomer].toSorted((a, b) => b[1] - a[1]);
console.log(top[0]); // [ 'Aigerim', 35500 ]
// 5. Unique customers — a Set
const customers = new Set(orders.map((order) => order.customer));
console.log(customers.size); // 4node group-orders.mjs. If the output matches the comments, the solution works.If the group key should not be a string — a number, true/false or an object — use Map.groupBy: it works the same way but returns a Map, and the keys are not turned into strings.
How this was verified2 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.