JavaScript Objects, Arrays and Array Methods (map, filter, reduce)
ProgrammingLesson 2 of 45 min read
What you'll learn
- Create, read and update objects and arrays, including destructuring and spread
- Transform data with map, filter, reduce, find, some, every and sort
- Tell a shallow copy from a deep copy and avoid accidental mutation
- Use optional chaining and nullish coalescing on data that might be missing
On this page
Here is a question that decides whether a warehouse picker walks to the wrong aisle: given a list of items, which are in stock, where do they live, and what is the total value on the shelves? In JavaScript the list is an array, each item is an object, and the answers come from four or five array methods chained together. This lesson is about reading and writing that kind of data fluently.
Starting from the data
An object is a collection of named properties; an array is an ordered list. Real data is almost always an array of objects, like rows from a database. Here is a small stock list and the questions a warehouse system would ask of it.
const stock = [
{ sku: "BOLT-8", bin: "A1", qty: 240, unitCents: 12, tags: ["metal", "small"] },
{ sku: "PIPE-2", bin: "B3", qty: 0, unitCents: 950, tags: ["plastic"] },
{ sku: "GLUE-1", bin: "A2", qty: 18, unitCents: 399, tags: [] },
{ sku: "TAPE-5", bin: "C1", qty: 64, unitCents: 150, tags: ["small"] },
];
const inStock = stock.filter((item) => item.qty > 0);
const labels = inStock.map((item) => `${item.sku}@${item.bin}`);
const valueCents = stock.reduce((sum, item) => sum + item.qty * item.unitCents, 0);
console.log(labels);
console.log(valueCents);
console.log(stock.find((item) => item.sku === "GLUE-1")?.bin);
console.log(stock.find((item) => item.sku === "NOPE-0")?.bin);
console.log(stock.some((item) => item.qty === 0), stock.every((item) => item.unitCents > 10));
console.log(stock.findIndex((item) => item.bin === "C1"));
const byValue = [...stock].sort((a, b) => b.qty * b.unitCents - a.qty * a.unitCents);
console.log(byValue.map((i) => i.sku));
console.log(stock[0].sku); // original order untouched because we sorted a copy
console.log(stock[1].supplier?.name ?? "no supplier");
console.log(stock.flatMap((i) => i.tags));
console.log([...new Set(stock.flatMap((i) => i.tags))]);Output
[ 'BOLT-8@A1', 'GLUE-1@A2', 'TAPE-5@C1' ] 19662 A2 undefined true true 3 [ 'TAPE-5', 'GLUE-1', 'BOLT-8', 'PIPE-2' ] BOLT-8 no supplier [ 'metal', 'small', 'plastic', 'small' ] [ 'metal', 'small', 'plastic' ]
Each method answers a different question. They all take a callback that receives one item at a time, and none of the first six changes the original array.
| Method | Returns | Use it when you want to |
|---|---|---|
| map | A new array, same length | Convert every item into something else |
| filter | A new, shorter array | Keep only items that pass a test |
| find / findIndex | One item (or index), else undefined / -1 | Locate the first match |
| some / every | A boolean | Ask whether any, or all, items pass a test |
| reduce | Any single value | Collapse a list into a total, object or map |
| flatMap | A new flattened array | Map and merge nested lists in one step |
| sort | The same array, reordered in place | Order items (copy first!) |
Taking objects apart and putting them back together
Destructuring pulls values out of an object or array in one line, and the spread operator ... copies or merges them. Together they let you describe an update as "the same object with one field changed" instead of editing it in place, which is the style React and most state libraries expect.
const stock = [
{ sku: "BOLT-8", bin: "A1", qty: 240 },
{ sku: "GLUE-1", bin: "A2", qty: 18 },
{ sku: "TAPE-5", bin: "C1", qty: 64 },
];
const { sku, ...rest } = stock[1];
console.log(sku, rest);
const names = stock.map(({ sku }) => sku);
console.log(names);
// immutable update: a new array, one new object
const restocked = stock.map((i) => (i.sku === "GLUE-1" ? { ...i, qty: i.qty + 100 } : i));
console.log(restocked[1], stock[1]);
// group by aisle letter with reduce
const byAisle = stock.reduce((acc, item) => {
const aisle = item.bin[0];
(acc[aisle] ??= []).push(item.sku);
return acc;
}, {});
console.log(byAisle);
const bins = Object.fromEntries(stock.map((i) => [i.sku, i.bin]));
for (const [name, bin] of Object.entries(bins)) {
if (bin.startsWith("A")) console.log(name, "is in aisle A");
}Output
GLUE-1 { bin: 'A2', qty: 18 }
[ 'BOLT-8', 'GLUE-1', 'TAPE-5' ]
{ sku: 'GLUE-1', bin: 'A2', qty: 118 } { sku: 'GLUE-1', bin: 'A2', qty: 18 }
{ A: [ 'BOLT-8', 'GLUE-1' ], C: [ 'TAPE-5' ] }
BOLT-8 is in aisle A
GLUE-1 is in aisle AWhen should you use these methods and when a plain loop? Use a method when its name states your intent: filter for "keep these", find for "first match", some for "is there any". Use for...of when you need break, continue or an early return, since callbacks to map and forEach cannot stop the iteration. Chains such as items.filter(...).map(...).reduce(...) are fine for short lists and read well, but each step builds a new array, so on very large lists a single loop can be both clearer and cheaper. Measure before optimising; readability usually wins.
References, copies and why "it changed by itself"
Objects and arrays are stored by reference. const b = a does not copy; both names point at one object, and {} === {} is false because they are two different objects. Spread ({ ...a }) and slice() make shallow copies: the top level is new, but nested objects are still shared. For a full copy use structuredClone.
const a = { qty: 1, dims: { w: 2 } };
const shallow = { ...a };
shallow.dims.w = 99;
console.log(a.dims.w); // changed through the shared inner object
const deep = structuredClone(a);
deep.dims.w = 5;
console.log(a.dims.w, deep.dims.w);
console.log([10, 9, 1, 100].sort());
console.log([10, 9, 1, 100].sort((x, y) => x - y));
console.log([1, 2, 3].map(parseInt));
console.log(["1", "2", "3"].map(Number));
try {
[].reduce((x, y) => x + y);
} catch (e) {
console.log(e.name + ": " + e.message);
}Output
99 99 5 [ 1, 10, 100, 9 ] [ 1, 9, 10, 100 ] [ 1, NaN, NaN ] [ 1, 2, 3 ] TypeError: Reduce of empty array with no initial value
The parseInt line is an old favourite: map passes (value, index) and parseInt reads the index as a radix, so "2" is parsed in base 1 and gives NaN. Pass a function that takes exactly what you need, or use Number.
Common mistakes and the errors they produce
- Reading a property of `undefined`.
TypeError: Cannot read properties of undefined (reading 'name')appears whenstock[1].supplier.nameis evaluated and there is no supplier. Use?.and provide a default with??. - Forgetting to return inside `map`. With braces
{}an arrow function needs an explicitreturn; otherwise you get an array full ofundefined. - `reduce` without an initial value on an empty array. Throws
TypeError: Reduce of empty array with no initial value. Always pass the starting value, such as0. - Mutating state you were given. Sorting or pushing into an array a function received alters the caller's data. Copy first with
[...items]. - Expecting `Object.freeze` to be deep. It protects only the top level, and in sloppy mode an assignment to a frozen object fails silently instead of throwing.
- Using `for...in` on arrays. It iterates keys as strings, including inherited ones. Use
for...ofor an array method.
How this shows up in real projects and interviews
API responses are arrays of objects, so a large share of front-end and Node.js code is the pipeline you just wrote: filter, map, reduce, group. Code reviewers watch for accidental mutation and for reduce used where a plain loop would be clearer. Interview warm-ups such as "group these records by a field", "remove duplicates" and "flatten this nested array" are tests of exactly these methods, so practise writing reduce and Set solutions from memory. The same thinking carries over to arrays and strings problems in any language, and to React state updates in state with useState.
Try it yourself
Using the stock array from warehouse.js, write code that (1) returns the SKUs that need reordering (qty below 25), (2) builds an object mapping each aisle letter to the total quantity stored there, and (3) returns a new array where every item has a valueCents property, without changing the original objects. Print the three results and confirm stock itself is unchanged.
Show a hint
For (2) use reduce with acc[aisle] = (acc[aisle] ?? 0) + item.qty. For (3) map to { ...item, valueCents: item.qty * item.unitCents }.
Key takeaways
map,filter,find,some,everyandreducecover most data-shaping tasks and do not mutate the original.sortmutates and sorts as strings by default; pass a comparator and copy first.- Objects and arrays are references; spread and
sliceare shallow,structuredCloneis deep. - Use
?.and??for data that might be missing, and always givereducean initial value. - Prefer "new object with one change" over editing shared objects in place.
Test yourself
A trick from this lesson's ride: Bare sort() sorts as text: compare by letters, not by size Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between map and forEach in JavaScript?
map returns a new array built from the values your callback returns, while forEach returns nothing and is only for side effects such as logging. Use map when you want a transformed array and forEach (or for...of) when you just want to do something with each item.
How do I copy an object or array in JavaScript without changing the original?
For a shallow copy use { ...obj } or [...arr]. That copies the top level only, so nested objects are still shared. For a full independent copy use structuredClone(value), which also handles dates, maps and sets but cannot copy functions.
Why does JavaScript sort([10, 9, 1, 100]) give the wrong order?
With no comparator, sort converts items to strings and compares them alphabetically, so "10" comes before "9". Pass a comparator for numbers, such as arr.sort((a, b) => a - b), to sort by numeric value.