JavaScript Arrays - Exercise 2

map() creates a new array by applying a callback function to every element in the original. The original array is not modified. Whatever you return from the callback becomes the corresponding element in the new array. It's one of the most useful tools in JavaScript - great for transforming data without side effects.

javascript

const numbers = [1, 2, 3, 4, 5];

// Double each number
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// Original is untouched
console.log(numbers); // [1, 2, 3, 4, 5]

// Transform objects
const users = [{ name: 'Alice' }, { name: 'Bob' }];
const names = users.map((user) => user.name);
console.log(names); // ['Alice', 'Bob']

filter() returns a new array containing only the elements that pass a test defined in your callback. If the callback returns a truthy value, the element is kept; if it returns falsy, it's excluded. The original array stays intact, and the result can have fewer elements - but never more.

javascript

const scores = [45, 72, 88, 30, 95, 60];

// Keep only passing scores (>= 60)
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [72, 88, 95, 60]

// Filter objects by a property
const products = [
    { name: 'Laptop', inStock: true },
    { name: 'Mouse', inStock: false },
    { name: 'Keyboard', inStock: true },
];
const available = products.filter((p) => p.inStock);
console.log(available.map((p) => p.name)); // ['Laptop', 'Keyboard']

reduce() iterates over an array and boils it down to a single value. The callback receives an accumulator (the running result) and the current element. Whatever you return from the callback becomes the new accumulator for the next iteration. You can optionally pass a starting value as the second argument - and you almost always should, to avoid surprises with empty arrays.

javascript

const numbers = [10, 20, 30, 40];

// Sum all values
const total = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(total); // 100

// Find the maximum
const max = numbers.reduce((acc, curr) => (curr > acc ? curr : acc), -Infinity);
console.log(max); // 40

// Count occurrences
const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];
const counts = words.reduce((acc, word) => {
    acc[word] = (acc[word] || 0) + 1;
    return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, cherry: 1 }

splice() is the Swiss Army knife for in-place array changes - it can remove, replace, and insert elements all in one call. It modifies the original array and returns an array of any removed elements. The first argument is the start index, the second is how many elements to delete, and any additional arguments are values to insert at that position.

javascript

const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May'];

// Remove 2 elements starting at index 1
const removed = months.splice(1, 2);
console.log(removed); // ['Feb', 'Mar']
console.log(months);  // ['Jan', 'Apr', 'May']

// Insert without removing (deleteCount = 0)
months.splice(1, 0, 'Feb', 'Mar');
console.log(months); // ['Jan', 'Feb', 'Mar', 'Apr', 'May']

// Replace: remove 1 element and insert another
months.splice(2, 1, 'MARCH');
console.log(months); // ['Jan', 'Feb', 'MARCH', 'Apr', 'May']

slice() returns a shallow copy of a portion of an array between a start and end index. The end index is exclusive - the element at that index is not included. Crucially, slice() does not modify the original array. Negative indices count back from the end, which makes it handy for grabbing the last few elements.

javascript

const letters = ['a', 'b', 'c', 'd', 'e'];

console.log(letters.slice(1, 3));  // ['b', 'c'] - index 1 up to (not including) 3
console.log(letters.slice(2));     // ['c', 'd', 'e'] - from index 2 to end
console.log(letters.slice(-2));    // ['d', 'e'] - last two elements
console.log(letters.slice());     // ['a', 'b', 'c', 'd', 'e'] - full shallow copy

// Original is unchanged
console.log(letters); // ['a', 'b', 'c', 'd', 'e']

find() returns the first element in the array that satisfies the provided testing function. Unlike filter(), it stops as soon as it finds a match and returns that single element - not an array. If nothing matches, it returns undefined. This makes it efficient when you're looking for one specific item in a large dataset.

javascript

const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' },
];

const user = users.find((u) => u.id === 2);
console.log(user); // { id: 2, name: 'Bob' }

// Returns undefined when nothing matches
const missing = users.find((u) => u.id === 99);
console.log(missing); // undefined

// Works with primitives too
const nums = [5, 12, 8, 130, 44];
const firstBig = nums.find((n) => n > 10);
console.log(firstBig); // 12

findIndex() works exactly like find() - same callback, same early-exit behaviour - but it returns the index of the matching element rather than the element itself. If no match is found, it returns -1. It's particularly useful when you need to know where something is so you can update or remove it later.

javascript

const items = [
    { id: 10, name: 'Pen' },
    { id: 20, name: 'Pencil' },
    { id: 30, name: 'Ruler' },
];

const idx = items.findIndex((item) => item.id === 20);
console.log(idx); // 1

// Update the item using the found index
if (idx !== -1) {
    items[idx].name = 'Mechanical Pencil';
}
console.log(items[1].name); // 'Mechanical Pencil'

// Returns -1 when not found
console.log(items.findIndex((item) => item.id === 999)); // -1

flat() creates a new array with all sub-array elements concatenated into it up to a specified depth. The depth defaults to 1 if you don't pass an argument. Pass Infinity to flatten completely regardless of how deeply nested things are. Like most non-mutating array methods, the original is left unchanged.

javascript

const nested = [1, [2, 3], [4, [5, 6]]];

console.log(nested.flat());    // [1, 2, 3, 4, [5, 6]] - depth 1 (default)
console.log(nested.flat(2));   // [1, 2, 3, 4, 5, 6]   - depth 2

// Flatten no matter how deep
const deep = [1, [2, [3, [4, [5]]]]];
console.log(deep.flat(Infinity)); // [1, 2, 3, 4, 5]

// Original untouched
console.log(nested); // [1, [2, 3], [4, [5, 6]]]

includes() checks whether an array contains a particular value and returns true or false. It uses the SameValueZero comparison (similar to === but handles NaN correctly - unlike indexOf, which can't detect NaN). You can also pass a second argument to start the search from a specific index.

javascript

const fruits = ['apple', 'banana', 'cherry'];

console.log(fruits.includes('banana'));    // true
console.log(fruits.includes('mango'));     // false

// Start searching from index 2
console.log(fruits.includes('banana', 2)); // false - 'banana' is at index 1

// includes handles NaN, indexOf does not
const nums = [1, 2, NaN, 4];
console.log(nums.includes(NaN));   // true
console.log(nums.indexOf(NaN));    // -1 - indexOf can't find NaN

sort() sorts the elements of an array in place and returns the sorted array. By default, it converts elements to strings and sorts them by UTF-16 code units - which means [10, 9, 2].sort() gives you [10, 2, 9], not [2, 9, 10]. For anything other than simple alphabetical sorting, always provide a comparator function.

javascript

// Default sort (strings): works fine for words
const fruits = ['banana', 'apple', 'cherry'];
fruits.sort();
console.log(fruits); // ['apple', 'banana', 'cherry']

// Default sort (numbers): DON'T do this
const nums = [10, 9, 2, 100];
nums.sort();
console.log(nums); // [10, 100, 2, 9] - wrong!

// Correct numeric sort with comparator
nums.sort((a, b) => a - b);
console.log(nums); // [2, 9, 10, 100] - correct ascending

// Descending order
nums.sort((a, b) => b - a);
console.log(nums); // [100, 10, 9, 2]