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,everyreducesortand 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 array | Returns a new array |
|---|---|
push, pop, shift, unshift | map, filter, slice, concat |
splice, sort, reverse, fill | toSorted, 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 see | Cause | Fix |
|---|---|---|
map returns [undefined, ...] | Callback has no return | Return the value |
forEach result is undefined | forEach returns nothing | Use map |
Sorting numbers gives 1, 10, 2 | Default sort is lexicographic | Pass a comparator |
| The original array changed | sort, reverse, splice mutate | Copy first with [...arr] |
| A copied object still shares data | Spread is a shallow copy | Copy nested objects too |
Cannot read properties of undefined in a chain | An intermediate value is missing | Optional chaining ?. |
sort mutates and sorts as text. [1, 10, 2].sort() gives [1, 10, 2] — both facts surprise people at once.
Common mistakes
sortorreversemutating an array that is used elsewheresortwith no comparator on numbersreducewith no initial value- Assuming spread is a deep copy
JSON.parse(JSON.stringify(...))silently losingDateobjects- Not checking
findforundefined filter(...).length > 0instead ofsome- A nested
filterinside amapwhere a lookup would do - Destructuring a nested property that may be missing
- Blanket
?.on values that must exist for...inover an arrayhasOwnPropertycalled directly on an untrusted object
Practice
- Build a report from the student array: names of 10th-class students who passed, sorted by marks descending.
- Sort the array with
sortand confirm the original is now sorted too. Fix it with a spread copy, then withtoSorted. - Sort
[10, 9, 100, 2]with no comparator and explain the result. - Sort students by class, then section, then name using a
||chain oflocaleCompare. - Compute class-wise totals, averages and pass counts with
reduce. - Call
reduceon an empty array with and without an initial value. Record both outcomes. - Group students by class with
reduce, then withObject.groupBy, then with aMap. Compare. - Spread-copy a student, change
address.city, and confirm the original changed. Fix it withstructuredClone, then with nested spread. - Deep-copy an object containing a
DateusingJSON.parse(JSON.stringify(...))and confirm the type is lost. Repeat withstructuredClone. - Join students and payments with a nested
filterinside amap, then with aMaplookup. Time both over 1,000 × 5,000 items. - Destructure a nested
address.citywhereaddressisundefined. Record the error, then fix it two ways. - 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,reduceandforEach - Sort numbers correctly
- Copy an array or object without sharing state
- Use optional chaining to survive missing data
Review questions
- Which array methods mutate, and why does it matter?
- Why must
reducebe given an initial value? - What does spread copy, and what does it share?
- When is optional chaining the wrong tool?
Next: The DOM and events