Skip to main content
Published / updated

Arrays and Objects

Before you start

You need: JavaScript fundamentals (Article 07).

Time: about 45 minutes, plus the practice.

Learning objective

Transform, filter and aggregate collections without loops, and copy objects without the shared-reference bugs that copying invites.

Topics

  • Array basics and mutation
  • map, filter, find, some, every
  • reduce
  • sort and its two traps
  • Objects, destructuring and spread
  • Shallow versus deep copy
  • Optional chaining
  • Grouping and lookups
  • Iterating objects

Arrays

const students = [
{ id: 1, name: 'Ravi Kumar', rollNumber: 'NCA-2024-0012', className: '10th', marks: 87 },
{ id: 2, name: 'Priya Sharma', rollNumber: 'NCA-2024-0018', className: '10th', marks: 91 },
{ id: 3, name: 'Arjun Reddy', rollNumber: 'NCA-2024-0031', className: '9th', marks: 65 }
];
Mutates the arrayReturns a new array
push, pop, shift, unshiftmap, filter, slice, concat
splice, sort, reverse, filltoSorted, toReversed, with

Knowing which is which prevents most array bugs. sort and reverse mutate in place and also return the array, which makes them easy to use accidentally:

const sorted = students.sort((a, b) => b.marks - a.marks);
// `students` is now sorted too — the original order is gone

const safe = [...students].sort((a, b) => b.marks - a.marks);
const alsoSafe = students.toSorted((a, b) => b.marks - a.marks); // modern

If the original order mattered anywhere — a list rendered elsewhere, a cached response — it has been destroyed silently.

map, filter, find

// map — transform every item, same length out
const names = students.map(student => student.name);

const rows = students.map(student => ({
label: `${student.name} (${student.rollNumber})`,
marks: student.marks
}));

// filter — keep matching items
const tenthClass = students.filter(student => student.className === '10th');
const passed = students.filter(student => student.marks >= 35);

// find — the first match, or undefined
const ravi = students.find(student => student.rollNumber === 'NCA-2024-0012');

if (ravi === undefined) {
// not found
}

// findIndex — position, or -1
const index = students.findIndex(student => student.id === 2);

// some / every — booleans, short-circuit
const anyFailed = students.some(student => student.marks < 35);
const allPassed = students.every(student => student.marks >= 35);

// includes — for primitives
const classNames = ['9th', '10th', '11th'];
classNames.includes('10th'); // true

find returns undefined when nothing matches — always check before using the result. filter returns an empty array, which is safe to iterate.

some and every stop at the first decisive item, so they beat filter(...).length > 0.

Chaining is the normal way to express a pipeline:

const topTenth = students
.filter(student => student.className === '10th')
.filter(student => student.marks >= 35)
.sort((a, b) => b.marks - a.marks)
.slice(0, 5)
.map(student => student.name);

Each step returns a new array, so nothing is mutated. Readable, and for a page of rows the cost is irrelevant — optimise only when a measurement says to.

reduce

The general-purpose one. Everything above can be built from it.

// Sum
const total = students.reduce((sum, student) => sum + student.marks, 0);

// Average, guarding against an empty array
const average = students.length === 0
? 0
: students.reduce((sum, s) => sum + s.marks, 0) / students.length;

// Maximum by a property
const topper = students.reduce(
(best, student) => student.marks > best.marks ? student : best
);

// Group by class
const byClass = students.reduce((groups, student) => {
const key = student.className;

if (groups[key] === undefined) {
groups[key] = [];
}

groups[key].push(student);
return groups;
}, {});

// { '10th': [Ravi, Priya], '9th': [Arjun] }

// Lookup by id
const byId = students.reduce((lookup, student) => {
lookup[student.id] = student;
return lookup;
}, {});

Always supply the initial value. Without it, reduce uses the first element as the seed — which throws on an empty array and produces the wrong type when accumulating into an object.

[].reduce((a, b) => a + b); // TypeError: Reduce of empty array with no initial value
[].reduce((a, b) => a + b, 0); // 0

Modern alternatives for grouping and lookup:

const byClass = Object.groupBy(students, student => student.className);

const byId = new Map(students.map(student => [student.id, student]));
byId.get(2);

Map beats a plain object for lookups: any key type, a real size, guaranteed insertion order, and no collision with inherited property names like constructor.

sort

Two traps, both common.

It sorts as strings by default

[10, 9, 100, 2].sort(); // [10, 100, 2, 9] — string comparison
[10, 9, 100, 2].sort((a, b) => a - b); // [2, 9, 10, 100]

Always pass a comparator for numbers.

students.sort((a, b) => a.marks - b.marks); // ascending
students.sort((a, b) => b.marks - a.marks); // descending

// Strings — localeCompare handles case and accents correctly
students.sort((a, b) => a.name.localeCompare(b.name));

// Dates
payments.sort((a, b) => new Date(a.paidOn) - new Date(b.paidOn));

// Multiple keys
students.sort((a, b) =>
a.className.localeCompare(b.className) ||
a.section.localeCompare(b.section) ||
a.name.localeCompare(b.name)
);

The || chain works because localeCompare returns 0 for equal, which is falsy, so the next comparison runs.

It mutates

Covered above, and worth repeating: copy first, or use toSorted.

Objects

const student = {
id: 1,
name: 'Ravi Kumar',
rollNumber: 'NCA-2024-0012',
address: { city: 'Hyderabad', pinCode: '500001' },

getLabel() {
return `${this.name} (${this.rollNumber})`;
}
};

student.name;
student['name'];
student[propertyName]; // computed key
Object.keys(student); // ['id', 'name', ...]
Object.values(student);
Object.entries(student); // [['id', 1], ['name', 'Ravi Kumar'], ...]

Object.hasOwn(student, 'name'); // true — the modern check
'name' in student; // true, but also true for inherited properties

Object.hasOwn replaces hasOwnProperty and is safer — an object with a property literally named hasOwnProperty breaks the old form.

Destructuring

const { name, rollNumber } = student;
const { name: studentName } = student; // rename
const { section = 'A' } = student; // default
const { address: { city } } = student; // nested
const { id, ...rest } = student; // rest

const [first, second] = students;
const [, , third] = students; // skip
const [head, ...tail] = students;
// In parameters — self-documenting call sites
function renderCard({ name, rollNumber, className, section = 'A' }) { }

// In a callback
students.map(({ name, marks }) => `${name}: ${marks}`);

// Swap without a temporary
let a = 1, b = 2;
[a, b] = [b, a];

Destructuring a nested property throws when the parent is missing:

const { address: { city } } = student; // TypeError if address is undefined

const { address: { city } = {} } = student; // safe
const city = student.address?.city; // clearer

Spread and copying

const copy = { ...student };
const updated = { ...student, className: '11th' }; // later wins
const merged = { ...defaults, ...overrides };

const arrayCopy = [...students];
const combined = [...groupA, ...groupB];
const withNew = [...students, newStudent];

Spread is how you update immutably — essential in React, and good practice everywhere.

Shallow versus deep

const copy = { ...student };

copy.name = 'Changed';
console.log(student.name); // 'Ravi Kumar' — top level is independent

copy.address.city = 'Bengaluru';
console.log(student.address.city); // 'Bengaluru' — SHARED reference

Spread copies one level. Nested objects and arrays are shared, so mutating one through the copy changes the original. This is the source of many "why did that change" bugs.

const deep = structuredClone(student); // modern, handles Dates, Maps, cycles

const alsoDeep = JSON.parse(JSON.stringify(student)); // older, lossy

JSON.parse(JSON.stringify(...)) loses Date objects (they become strings), undefined values, functions, Map, Set, and throws on circular references. structuredClone handles all of those and is the right default.

Better still, avoid deep mutation:

const updated = {
...student,
address: { ...student.address, city: 'Bengaluru' }
};

Nothing is mutated, and the sharing question does not arise.

Optional chaining

const city = student?.address?.city; // undefined if either is missing
const first = students?.[0]?.name;
const result = student.getLabel?.(); // called only if it exists

const displayCity = student?.address?.city ?? 'Not recorded';

?. short-circuits to undefined rather than throwing.

Use it where absence is legitimate, not everywhere. Applied to something that must exist, it converts a loud crash into a silent wrong result:

// If the search box must exist, let it fail loudly
const term = document.getElementById('searchBox')?.value ?? '';
// A typo in the id now produces an empty search instead of an error

That is the same absent-versus-zero problem in a different form: a crash is found in five minutes, a silent empty result reaches a user.

Grouping and joining

// Two lists, joined in memory — the fix for an N+1
const payments = [
{ studentId: 1, amount: 5000 },
{ studentId: 1, amount: 3000 },
{ studentId: 2, amount: 8000 }
];

const paymentsByStudent = new Map();

for (const payment of payments) {
if (!paymentsByStudent.has(payment.studentId)) {
paymentsByStudent.set(payment.studentId, []);
}

paymentsByStudent.get(payment.studentId).push(payment);
}

const withPayments = students.map(student => ({
...student,
payments: paymentsByStudent.get(student.id) ?? [],
totalPaid: (paymentsByStudent.get(student.id) ?? [])
.reduce((sum, p) => sum + p.amount, 0)
}));

Build a lookup once, then map — O(n + m) rather than the O(n × m) of a nested filter inside a map.

// Unique values
const classNames = [...new Set(students.map(s => s.className))];

// Unique objects by a key
const uniqueById = [...new Map(students.map(s => [s.id, s])).values()];

Iterating objects

for (const [key, value] of Object.entries(student)) {
console.log(`${key}: ${value}`);
}

const upperCased = Object.fromEntries(
Object.entries(student).map(([key, value]) => [key, String(value).toUpperCase()])
);

Object.entries plus Object.fromEntries is the standard way to transform an object — there is no Object.map.

Avoid for...in on objects you did not create; it walks inherited enumerable properties too.

Errors you will hit

What you seeCauseFix
map returns [undefined, ...]Callback has no returnReturn the value
forEach result is undefinedforEach returns nothingUse map
Sorting numbers gives 1, 10, 2Default sort is lexicographicPass a comparator
The original array changedsort, reverse, splice mutateCopy first with [...arr]
A copied object still shares dataSpread is a shallow copyCopy nested objects too
Cannot read properties of undefined in a chainAn intermediate value is missingOptional chaining ?.

sort mutates and sorts as text. [1, 10, 2].sort() gives [1, 10, 2] — both facts surprise people at once.

Common mistakes

  • sort or reverse mutating an array that is used elsewhere
  • sort with no comparator on numbers
  • reduce with no initial value
  • Assuming spread is a deep copy
  • JSON.parse(JSON.stringify(...)) silently losing Date objects
  • Not checking find for undefined
  • filter(...).length > 0 instead of some
  • A nested filter inside a map where a lookup would do
  • Destructuring a nested property that may be missing
  • Blanket ?. on values that must exist
  • for...in over an array
  • hasOwnProperty called directly on an untrusted object

Practice

  1. Build a report from the student array: names of 10th-class students who passed, sorted by marks descending.
  2. Sort the array with sort and confirm the original is now sorted too. Fix it with a spread copy, then with toSorted.
  3. Sort [10, 9, 100, 2] with no comparator and explain the result.
  4. Sort students by class, then section, then name using a || chain of localeCompare.
  5. Compute class-wise totals, averages and pass counts with reduce.
  6. Call reduce on an empty array with and without an initial value. Record both outcomes.
  7. Group students by class with reduce, then with Object.groupBy, then with a Map. Compare.
  8. Spread-copy a student, change address.city, and confirm the original changed. Fix it with structuredClone, then with nested spread.
  9. Deep-copy an object containing a Date using JSON.parse(JSON.stringify(...)) and confirm the type is lost. Repeat with structuredClone.
  10. Join students and payments with a nested filter inside a map, then with a Map lookup. Time both over 1,000 × 5,000 items.
  11. Destructure a nested address.city where address is undefined. Record the error, then fix it two ways.
  12. Use ?. on an element id that does not exist and confirm the silent empty result. Then let it fail loudly instead.

Exercises 8 and 10 are the two that show up in real applications most often.

You can now

  • Transform and aggregate collections without loops
  • Choose between map, filter, reduce and forEach
  • Sort numbers correctly
  • Copy an array or object without sharing state
  • Use optional chaining to survive missing data

Review questions

  1. Which array methods mutate, and why does it matter?
  2. Why must reduce be given an initial value?
  3. What does spread copy, and what does it share?
  4. When is optional chaining the wrong tool?

Next: The DOM and events