JavaScript · lesson 9 of 24
Beginner
11- 1 Introduction to JavaScript
- 2 Setting Up JavaScript Development Environment
- 3 Variables in JavaScript: var, let, and const
- 4 JavaScript Data Types
- 5 JavaScript Operators
- 6 Control Flow in JavaScript
- 7 Functions in JavaScript
- 8 Working with Strings in JavaScript
- 9 Arrays in JavaScript
- 10 Objects in JavaScript
- 18 DOM Manipulation in JavaScript
Intermediate
10- 11 Error Handling in JavaScript
- 12 Object-Oriented Programming in JavaScript
- 13 Prototypes and Prototype Chain in JavaScript
- 14 Closures in JavaScript
- 15 JavaScript Event Loop Explained
- 16 Asynchronous JavaScript: Callbacks, Promises, and Async/Await
- 17 JavaScript Modules: ESM and CommonJS
- 19 Browser Web APIs in JavaScript
- 20 Testing JavaScript with Jest and Vitest
- 24 JavaScript Interview Preparation: Top 35 Questions
Advanced
3Arrays in JavaScript
Learn JavaScript arrays — creation, functional methods like map/filter/reduce, destructuring, sorting, and common transformation patterns.
By SkillByExample · Updated
Arrays are JavaScript’s go-to ordered collection. They can hold any mix of values, grow and shrink dynamically, and come with a rich set of built-in methods. The functional iteration methods — map, filter, reduce, and their companions — are the heart of modern JavaScript data transformation, letting you express what you want rather than spelling out every loop step by step.
Creating Arrays
The array literal [] is by far the most common way to create an array and should be your default. Array.from is the right tool when you need to create an array from something that isn’t already one — a string, a NodeList, or a range generated from a length.
// Array literal — the standard way
const fruits = ["apple", "banana", "cherry"];
const numbers = [1, 2, 3, 4, 5];
const mixed = [1, "hello", true, null, { id: 1 }]; // arrays can hold any type
// Array constructor (rarely needed — prefer the literal)
const empty = new Array(3); // [empty × 3] — length 3, no actual values
// Array.from — create from any iterable or array-like
const chars = Array.from("hello"); // ["h","e","l","l","o"]
const doubled = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6] — map in one step
const range = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]
// Spread to copy or combine
const copy = [...fruits];
const combined = [...fruits, ...numbers];
console.log(empty);
console.log(chars);
console.log(doubled);
console.log(range);
console.log(copy);
console.log(combined);
$ node create.js
[ <3 empty items> ]
[ 'h', 'e', 'l', 'l', 'o' ]
[ 2, 4, 6 ]
[ 1, 2, 3, 4, 5 ]
[ 'apple', 'banana', 'cherry' ]
[ 'apple', 'banana', 'cherry', 1, 2, 3, 4, 5 ]
new Array(3) creates an array of length 3 with no actual elements — every slot is a “hole”, which Node prints as <3 empty items> (most array methods still read a hole as undefined). Array.from is more useful in practice because it always produces real, indexable values. The spread operator [...fruits] is the idiomatic shallow copy, and combining arrays with [...a, ...b] replaces the older concat in modern code.
map — Transform Every Element
map is the go-to method when you want to convert each element of an array into something else. It always returns a new array of exactly the same length, with each element replaced by whatever your callback returns. It never modifies the original array. Use it whenever you need to reshape data — converting raw values to display strings, extracting fields from objects, or computing derived values.
const prices = [10, 25, 8, 42, 15];
// Apply a transformation to every element
const withTax = prices.map(p => p * 1.1);
const formatted = prices.map(p => `$${p.toFixed(2)}`);
console.log(withTax);
console.log(formatted);
// Real-world: transform an API response into a view model
const users = [
{ id: 1, firstName: "Alice", lastName: "Smith", age: 30 },
{ id: 2, firstName: "Bob", lastName: "Jones", age: 25 },
];
const viewModels = users.map(u => ({
id: u.id,
fullName: `${u.firstName} ${u.lastName}`, // combine fields
isAdult: u.age >= 18, // derive a boolean
}));
console.log(viewModels);
$ node map-demo.js
[ 11, 27.500000000000004, 8.8, 46.2, 16.5 ]
[ '$10.00', '$25.00', '$8.00', '$42.00', '$15.00' ]
[
{ id: 1, fullName: 'Alice Smith', isAdult: true },
{ id: 2, fullName: 'Bob Jones', isAdult: true }
]
map returns a new array where each element is the result of applying the callback. withTax’s second entry lands on 27.500000000000004, not a clean 27.5 — floating-point multiplication (25 * 1.1) doesn’t always land on an exact decimal, a real quirk worth seeing rather than a rounded fiction. The toFixed(2) call sidesteps that by converting to a fixed-precision string, so formatted produces clean currency strings — each map call creates a fresh array without touching prices. In the view-model example, arrow functions with implicit object returns (=> ({ ... })) transform each user object into a new shape with computed fields.
filter — Keep Matching Elements
filter returns a new array containing only the elements for which the callback returns a truthy value. It’s the right choice whenever you need a subset of a collection — removing out-of-stock items, keeping only recent records, or narrowing search results. Like map, it never touches the original array.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(n => n % 2 === 0);
const bigEvens = numbers.filter(n => n % 2 === 0 && n > 5);
console.log(evens);
console.log(bigEvens);
// Real-world: filter a product list by availability and price
const products = [
{ name: "Laptop", price: 999, inStock: true },
{ name: "Mouse", price: 29, inStock: false },
{ name: "Desk", price: 349, inStock: true },
{ name: "Chair", price: 199, inStock: true },
];
const affordable = products.filter(p => p.inStock && p.price < 500);
console.log(affordable);
$ node filter-demo.js
[ 2, 4, 6, 8, 10 ]
[ 6, 8, 10 ]
[
{ name: 'Desk', price: 349, inStock: true },
{ name: 'Chair', price: 199, inStock: true }
]
filter keeps only the elements the callback approves — evens drops every odd number, bigEvens narrows further to even numbers greater than 5. In the product example, Laptop fails the price cutoff and Mouse fails the stock check, leaving Desk and Chair.
reduce — Aggregate to a Single Value
reduce is the most flexible array method: it processes the entire array and accumulates a single result — a sum, an object, a nested structure, or even another array. The accumulator starts at the initial value you provide and is passed into each callback call along with the current element. It’s the tool to reach for when map and filter aren’t enough.
const cart = [
{ name: "Laptop", price: 999, qty: 1 },
{ name: "Mouse", price: 29, qty: 2 },
{ name: "Cable", price: 9, qty: 3 },
];
// Sum a computed value across all elements
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log(total);
// Group elements by a property — accumulator becomes an object
const orders = [
{ id: 1, status: "shipped" },
{ id: 2, status: "pending" },
{ id: 3, status: "shipped" },
{ id: 4, status: "pending" },
{ id: 5, status: "delivered" },
];
const grouped = orders.reduce((acc, order) => {
const key = order.status;
if (!acc[key]) acc[key] = [];
acc[key].push(order);
return acc;
}, {});
console.log(grouped);
// Flatten one level (flatMap is cleaner — see below)
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flat);
$ node reduce-demo.js
1084
{
shipped: [ { id: 1, status: 'shipped' }, { id: 3, status: 'shipped' } ],
pending: [ { id: 2, status: 'pending' }, { id: 4, status: 'pending' } ],
delivered: [ { id: 5, status: 'delivered' } ]
}
[ 1, 2, 3, 4, 5 ]
reduce threads an accumulator through the array, producing a single final value. The second argument 0 is the initial accumulator — for a sum it should always be 0. In the grouping example, the accumulator starts as {} and each iteration adds property keys with arrays of orders. The concat approach flattens one level, but flatMap or flat() is more idiomatic for that pattern.
Chaining map, filter, reduce
Because these methods all return new arrays (or a value), you can chain them together into a readable data pipeline. Each step clearly expresses one transformation, and the chain reads almost like prose. This is one of the most powerful patterns in modern JavaScript.
const transactions = [
{ amount: 120, type: "income" },
{ amount: 45, type: "expense" },
{ amount: 200, type: "income" },
{ amount: 80, type: "expense" },
{ amount: 60, type: "income" },
];
const netIncome = transactions
.filter(t => t.type === "income") // step 1: keep only income
.map(t => t.amount) // step 2: extract the amounts
.reduce((sum, amount) => sum + amount, 0); // step 3: sum them
console.log(netIncome); // 380
$ node chaining.js
380
Each method in the chain returns a value the next method can consume. filter returns a new array (3 elements), map on that array returns another array (3 amounts: 120, 200, 60), and reduce sums them to 380. The original transactions array is never modified — every step produces a fresh value.
find, findIndex, some, every
These four methods answer questions about the contents of an array without creating a new one. find and findIndex locate the first matching element. some and every test whether any or all elements satisfy a condition. All four stop as soon as they have their answer, so they’re efficient on large arrays.
const users = [
{ id: 1, name: "Alice", admin: false },
{ id: 2, name: "Bob", admin: true },
{ id: 3, name: "Carol", admin: false },
];
console.log(users.find(u => u.id === 2));
console.log(users.find(u => u.id === 99));
console.log(users.findIndex(u => u.name === "Carol"));
console.log(users.findIndex(u => u.name === "Dave"));
console.log(users.some(u => u.admin));
console.log(users.every(u => u.admin));
console.log(users.every(u => u.id > 0));
$ node search-demo.js
{ id: 2, name: 'Bob', admin: true }
undefined
2
-1
true
false
true
find returns the first matching element or undefined. findIndex returns the index of the first match or -1. some returns true if any element passes — and stops scanning immediately upon finding one. every returns true only if ALL elements pass — it also short-circuits, returning false the moment it encounters a failing element.
flat and flatMap
flat solves the common problem of arrays nested inside arrays — something that arises naturally from one-to-many relationships in data. flatMap combines a map and a flat(1) in a single, more efficient pass, making it the right tool when each element should expand into multiple results.
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // one level deep
console.log(nested.flat(2)); // two levels deep
console.log(nested.flat(Infinity)); // fully flattened regardless of depth
// flatMap = map + flat(1) in one efficient pass
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap(s => s.split(" "));
console.log(words);
// Real-world: expand one-to-many relationships
const categories = [
{ name: "JS", tags: ["frontend", "backend"] },
{ name: "Python", tags: ["backend", "data"] },
];
const allTags = categories.flatMap(c => c.tags);
console.log(allTags);
$ node flat-demo.js
[ 1, 2, 3, 4, [ 5, 6 ] ]
[ 1, 2, 3, 4, 5, 6 ]
[ 1, 2, 3, 4, 5, 6 ]
[ 'hello', 'world', 'foo', 'bar' ]
[ 'frontend', 'backend', 'backend', 'data' ]
flat() removes nesting one level at a time. Infinity flattens everything regardless of depth. flatMap does a map followed by a flat(1) in one pass — so s.split(" ") produces ["hello", "world"] from "hello world", and flatMap flattens those results into a single flat array of words without creating an intermediate nested array. For the categories example, each flatMap callback returns a full tags array, and those items get concatenated into the result.
Destructuring
Array destructuring lets you unpack values from an array into named variables in a single statement. This makes code that returns or passes multiple values much cleaner — no more result[0], result[1]. The rest syntax (...) collects any remaining elements into a new array.
const [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first); // 10
console.log(second); // 20
console.log(rest); // [30, 40, 50]
// Skip elements with empty commas
const [,, third] = [10, 20, 30];
console.log(third); // 30
// Swap variables without a temp variable
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Destructure a function's array return value
function minMax(arr) {
return [Math.min(...arr), Math.max(...arr)];
}
const [min, max] = minMax([3, 1, 4, 1, 5, 9]);
console.log(min, max); // 1 9
$ node destructuring.js
10
20
[ 30, 40, 50 ]
30
2 1
1 9
Destructuring assigns positionally from the left. The rest syntax ...rest captures remaining elements into a new array. Empty commas skip positions. The swap [a, b] = [b, a] works because the right-hand side is evaluated first — an array [2, 1] is created, then its elements are destructured into a and b in order. The minMax function returns [min, max] and the caller destructures those right back out — no intermediate indexing needed.
Sorting
sort reorders the elements of an array in place and returns the same array. This in-place mutation is the most important thing to know about it. The second most important: without a comparator function, sort converts everything to strings and compares them lexicographically — which produces wrong results for numbers.
const nums = [10, 2, 30, 4, 20];
// Wrong — lexicographic sort treats numbers as strings
console.log([...nums].sort());
// Correct — numeric ascending: subtract gives negative/zero/positive
console.log([...nums].sort((a, b) => a - b));
// Numeric descending
console.log([...nums].sort((a, b) => b - a));
// Sort objects by a property
const people = [
{ name: "Charlie", age: 35 },
{ name: "Alice", age: 28 },
{ name: "Bob", age: 42 },
];
console.log([...people].sort((a, b) => a.age - b.age)); // ascending by age
console.log([...people].sort((a, b) => a.name.localeCompare(b.name))); // alphabetical by name
$ node sort-demo.js
[ 10, 2, 20, 30, 4 ]
[ 2, 4, 10, 20, 30 ]
[ 30, 20, 10, 4, 2 ]
[
{ name: 'Alice', age: 28 },
{ name: 'Charlie', age: 35 },
{ name: 'Bob', age: 42 }
]
[
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 42 },
{ name: 'Charlie', age: 35 }
]
Without a comparator, sort converts every element to a string and compares UTF-16 code units — so 10 sorts before 2 because "1" < "2" lexicographically. The comparator (a, b) => a - b fixes this: negative when a < b (a before b), positive when a > b, zero when equal. For strings, localeCompare handles locale-specific ordering. Each call here spreads into a copy ([...nums], [...people]) before sorting — sort always mutates in place, so sorting the same array twice in a row (once by age, once by name) would silently discard the first ordering.
slice vs splice
slice and splice look similar but behave very differently. slice is non-destructive: it returns a portion of the array as a new array and leaves the original untouched. splice modifies the original array in place and is the tool for inserting or removing elements at a specific position.
const arr = [0, 1, 2, 3, 4, 5];
// slice — does NOT mutate, returns new array
console.log(arr.slice(2, 4)); // from index 2 up to (not including) 4
console.log(arr.slice(-2)); // last two elements
console.log(arr); // unchanged
// splice — MUTATES in place, returns the removed elements
const removed = arr.splice(2, 2); // remove 2 elements starting at index 2
console.log(removed);
console.log(arr); // original is modified
// splice to insert at a position
arr.splice(2, 0, 10, 11); // at index 2, remove 0 elements, insert 10 and 11
console.log(arr);
$ node slice-splice.js
[2, 3]
[4, 5]
[ 0, 1, 2, 3, 4, 5 ]
[2, 3]
[ 0, 1, 4, 5 ]
[ 0, 1, 10, 11, 4, 5 ]
slice(2, 4) returns a new array [2, 3] and leaves the original alone. slice(-2) returns the last two elements. splice(2, 2) is different: it removes two elements starting at index 2, returns the removed [2, 3], and mutates arr to [0, 1, 4, 5]. The second splice call with 0 as the delete count inserts new elements without removing anything, so arr becomes [0, 1, 10, 11, 4, 5].
Common Pitfalls
Mutating the original objects inside map:
// Objects in arrays are references — creating a new array doesn't clone the objects
const users = [{ name: "Alice" }, { name: "Bob" }];
const names = users.map(u => {
u.active = true; // mutates the original objects — side effect!
return u.name;
});
console.log(names);
console.log(users); // the "original" objects now carry `active` too
// Fix: create new objects with spread instead of modifying in place
const safe = users.map(u => ({ ...u, active: true }));
console.log(safe);
$ node pitfalls.js
[ 'Alice', 'Bob' ]
[ { name: 'Alice', active: true }, { name: 'Bob', active: true } ]
[ { name: 'Alice', active: true }, { name: 'Bob', active: true } ]
The map callback mutates the original users array elements (they’re references, not copies) — a side effect that can be hard to track: users itself now carries active: true even though map “just” builds names. The safe approach with { ...u, active: true } creates a new object, leaving the original untouched (here it looks the same only because users was already mutated by the first example — in real code that wouldn’t happen).
sort mutates in place — sort a copy when you need the original:
const original = [3, 1, 2];
const sorted = [...original].sort((a, b) => a - b);
// original is still [3, 1, 2]
Array.from({ length: n }) gives undefined elements, not null:
Array.from({ length: 3 }); // [undefined, undefined, undefined]
Array.from({ length: 3 }, () => 0); // [0, 0, 0] — use a mapper for a real value
$ node array-from.js
[ undefined, undefined, undefined ]
[ 0, 0, 0 ]
Array.from({ length: 3 }) creates an array of undefined values — each slot is a real element, but its value is undefined. By providing a mapper function () => 0, every element becomes 0 instead. This matters for methods like map that skip “holes” but process undefined values differently.
Practice
-
Shopping cart total: Given an array of cart items
[{name: "Apple", price: 1.20, qty: 3}, ...], usereduceto compute the total cost (price × qty for each item, then sum all). -
Filter and chain: From an array of temperatures
[72, 65, 80, 55, 90, 45], usefilterto keep days above 70, thenmapto convert each to an object{ fahrenheit: n, celsius: (n - 32) * 5/9 }. -
Find by property: Given
[{id: 1, name: "Alice"}, {id: 2, name: "Bob"}, {id: 3, name: "Carol"}], usefindto get the user withid === 2, thenfindIndexto get the position of the user named “Carol”. -
Deep flatten: Given
[[1, 2], [3, [4, 5]], [6, [7, [8, 9]]]], useflat(Infinity)to produce a single flat array[1, 2, 3, 4, 5, 6, 7, 8, 9]. Then write a manual reducer version that achieves the same thing. -
Sort with care: Sort
["banana", "apple", "cherry", "date"]alphabetically. Then sort an array of objects[{name: "Zara", age: 25}, {name: "Alice", age: 30}, {name: "Bob", age: 25}]first by age ascending, with ties broken alphabetically by name — without mutating the original array.
Solution 1
```js const cart = [ { name: "Apple", price: 1.20, qty: 3 }, { name: "Bread", price: 3.50, qty: 1 }, { name: "Milk", price: 2.00, qty: 2 }, ];const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log($${total.toFixed(2)});
</details>
<details><summary>Solution 2</summary>
```js
const temps = [72, 65, 80, 55, 90, 45];
const result = temps
.filter(t => t > 70)
.map(t => ({
fahrenheit: t,
celsius: Math.round((t - 32) * 5 / 9 * 10) / 10
}));
console.log(result);
Solution 3
```js const users = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Carol" }, ];const user = users.find(u => u.id === 2); const index = users.findIndex(u => u.name === “Carol”);
console.log(user); // { id: 2, name: ‘Bob’ } console.log(index); // 2
</details>
<details><summary>Solution 4</summary>
```js
const nested = [[1, 2], [3, [4, 5]], [6, [7, [8, 9]]]];
// Built-in
const flat1 = nested.flat(Infinity);
// Manual reducer
function deepFlatten(arr) {
return arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(deepFlatten(val)) : acc.concat(val)
, []);
}
const flat2 = deepFlatten(nested);
console.log(flat1); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(flat2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Solution 5
```js const fruits = ["banana", "apple", "cherry", "date"]; const sorted = [...fruits].sort(); console.log(sorted); // ['apple', 'banana', 'cherry', 'date']const people = [ { name: “Zara”, age: 25 }, { name: “Alice”, age: 30 }, { name: “Bob”, age: 25 }, ];
const sortedPeople = […people].sort((a, b) => { if (a.age !== b.age) return a.age - b.age; return a.name.localeCompare(b.name); }); console.log(sortedPeople); // [{name: “Bob”, age: 25}, {name: “Zara”, age: 25}, {name: “Alice”, age: 30}]
</details>