How to buildOctober 11, 20268 min read
JavaScript from scratchStep 5 of 9

Arrays, objects, Map and Set: data structures in practice

In short

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?

FundamentalsLevel: BeginnerData Structures

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

Verified with

Node.js 24.18 LTS (V8 13.6)ECMAScript 2025

Checked against docs: October 11, 2026

Contents
  1. 01Four structures, four ways to find data
  2. 02Array: order, indexes and methods
  3. 03Object: a record with fields
  4. 04Map vs. object: which one when
  5. 05Set: uniqueness and the "is it there" check
  6. 06Copying and mutation: spread and structuredClone
  7. 07Practice: grouping orders by status

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
Built-in JavaScript structures and their strengths

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
arrays.mjs
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 ]
Run: 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.

objects.mjs
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 status
Run: node 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.keys or Object.entries
Map
Directly in for...of

Frequent additions and removals

Object
Not optimized for them
Map
Performs better

JSON

Object
JSON.stringify out of the box
Map
Needs converting first
Based on the "Objects vs. Maps" table in the MDN documentation
map.mjs
// 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}
Run: 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.

set.mjs
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 ]
Run: 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.

copy.mjs
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 ]
Run: 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, with instead of sort, 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.

  1. Group

    Object.groupBy puts 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.

  2. Compute totals

    Object.entries turns the groups into pairs, map builds a report row, reduce sums the orders inside a group.

  3. Collect revenue per customer

    Customers appear as we iterate, so we accumulate sums in a Map and skip cancelled orders.

  4. Sort

    Spread turns the Map into an array of pairs, and we sort a copy by revenue.

  5. Count the unique ones

    A Set of customer names removes the duplicates by itself; the answer is in size.

group-orders.mjs
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); // 4
Run: node 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.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.