JavaScript Arrays - Exercise 3
Array destructuring lets you unpack values from an array into distinct variables using a concise syntax that mirrors array literal notation. The position of a variable in the pattern determines which element it receives. You can skip elements with commas, set default values, and use the rest syntax (...rest) to collect remaining items into a new array.
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
console.log(red, green, blue); // 255 128 0
// Skip elements
const [first, , third] = [10, 20, 30];
console.log(first, third); // 10 30
// Default values
const [a = 1, b = 2] = [42];
console.log(a, b); // 42 2
// Rest syntax
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Swap variables without a temp variable
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
The spread operator (...) expands an array's elements in place. It's perfect for copying arrays, merging them, or passing their elements as individual function arguments - all without mutating the originals. Because it creates a shallow copy, nested objects inside the array still share references.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// Merge arrays
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]
// Copy an array (shallow)
const copy = [...arr1];
copy.push(99);
console.log(arr1); // [1, 2, 3] - original unchanged
console.log(copy); // [1, 2, 3, 99]
// Insert elements in the middle
const inserted = [...arr1, 10, 11, ...arr2];
console.log(inserted); // [1, 2, 3, 10, 11, 4, 5, 6]
// Spread into a function
const nums = [3, 1, 4, 1, 5, 9];
console.log(Math.max(...nums)); // 9
Array.from() creates a new, shallow-copied array from an array-like or iterable object. It handles things that look like arrays (NodeLists, strings, arguments objects, Sets, Maps) and turns them into real arrays. The optional second argument is a mapping function, making it a handy way to both convert and transform in a single step.
// From a string
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']
// From a Set (removes duplicates)
const unique = Array.from(new Set([1, 2, 2, 3, 3, 3]));
console.log(unique); // [1, 2, 3]
// From a length + map function - generate sequences
const squares = Array.from({ length: 5 }, (_, i) => i * i);
console.log(squares); // [0, 1, 4, 9, 16]
// From a Map
const map = new Map([['a', 1], ['b', 2]]);
console.log(Array.from(map)); // [['a', 1], ['b', 2]]
reduce() really shines when the result you're building is more complex than a simple sum. Because the accumulator can be anything - an object, an array, a Map - you can use it to group data, restructure arrays, or implement pipelines. The key insight is that your accumulator's shape is entirely up to you; define it with the initial value you pass as the second argument.
const orders = [
{ category: 'food', amount: 30 },
{ category: 'tech', amount: 200 },
{ category: 'food', amount: 50 },
{ category: 'tech', amount: 100 },
{ category: 'books', amount: 25 },
];
// Group and sum by category
const totals = orders.reduce((acc, order) => {
acc[order.category] = (acc[order.category] || 0) + order.amount;
return acc;
}, {});
console.log(totals); // { food: 80, tech: 300, books: 25 }
// Flatten an array of arrays using reduce
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flat); // [1, 2, 3, 4, 5, 6]
flatMap() does a map() followed by a one-level flat() - but in a single pass, which makes it slightly more efficient. It's especially useful when your mapping function needs to return a variable number of elements for each input (zero, one, or many). If you return an empty array from the callback, that element is effectively filtered out.
// Expand each number into itself and its double
const nums = [1, 2, 3];
const expanded = nums.flatMap((n) => [n, n * 2]);
console.log(expanded); // [1, 2, 2, 4, 3, 6]
// Split sentences into words
const sentences = ['hello world', 'foo bar baz'];
const words = sentences.flatMap((s) => s.split(' '));
console.log(words); // ['hello', 'world', 'foo', 'bar', 'baz']
// Filter and transform in one step (return [] to skip)
const mixed = [1, -2, 3, -4, 5];
const positiveDoubled = mixed.flatMap((n) => n > 0 ? [n * 2] : []);
console.log(positiveDoubled); // [2, 6, 10]
every() tests whether all elements in an array pass a given condition. It returns true only if the callback returns truthy for every single element. The moment it encounters a falsy result it short-circuits - stops iterating and immediately returns false. Calling every() on an empty array always returns true (vacuously true).
const ages = [21, 25, 30, 18, 22];
// Check if all users are adults
const allAdults = ages.every((age) => age >= 18);
console.log(allAdults); // true
const mixed = [21, 25, 15, 18];
console.log(mixed.every((age) => age >= 18)); // false - 15 fails
// Validate all fields are filled
const formFields = ['Alice', 'alice@example.com', '555-1234'];
const allFilled = formFields.every((field) => field.trim().length > 0);
console.log(allFilled); // true
// Vacuously true for empty arrays
console.log([].every((x) => x > 100)); // true
some() is the counterpart to every() - it returns true if at least one element passes the test. It also short-circuits: as soon as it finds one truthy result, it stops and returns true without checking the rest. On an empty array it always returns false (there's no element that could satisfy the condition).
const inventory = [
{ name: 'Apples', qty: 5 },
{ name: 'Bananas', qty: 0 },
{ name: 'Oranges', qty: 12 },
];
// Is anything out of stock?
const hasOutOfStock = inventory.some((item) => item.qty === 0);
console.log(hasOutOfStock); // true
// Does any score exceed 90?
const scores = [70, 85, 55, 92, 60];
console.log(scores.some((s) => s > 90)); // true
console.log(scores.some((s) => s > 100)); // false
// Empty array always returns false
console.log([].some((x) => x > 0)); // false
Object.entries() returns an array of [key, value] pairs, which opens the door to using all the powerful array methods on an object's data. Combine it with filter(), map(), or sort(), then use Object.fromEntries() to convert the result back into an object. It's one of the cleanest patterns for transforming objects without writing imperative loops.
const prices = { apple: 1.5, banana: 0.5, cherry: 3.0, mango: 2.5 };
// Filter only expensive items (price > 1)
const expensive = Object.fromEntries(
Object.entries(prices).filter(([, price]) => price > 1)
);
console.log(expensive); // { apple: 1.5, cherry: 3.0, mango: 2.5 }
// Apply a 10% discount to every item
const discounted = Object.fromEntries(
Object.entries(prices).map(([name, price]) => [name, +(price * 0.9).toFixed(2)])
);
console.log(discounted); // { apple: 1.35, banana: 0.45, cherry: 2.7, mango: 2.25 }
// Sort object by value
const sorted = Object.fromEntries(
Object.entries(prices).sort(([, a], [, b]) => a - b)
);
console.log(sorted); // { banana: 0.5, apple: 1.5, mango: 2.5, cherry: 3.0 }
Because filter(), map(), sort(), and most other array methods return a new array, you can chain them - calling one method directly on the result of another. This produces readable, declarative pipelines. The order matters: filter before map to avoid processing elements you'll discard, and sort after filter/map if sorting the transformed data.
const employees = [
{ name: 'Alice', dept: 'Engineering', salary: 90000 },
{ name: 'Bob', dept: 'Marketing', salary: 60000 },
{ name: 'Carol', dept: 'Engineering', salary: 95000 },
{ name: 'Dave', dept: 'Engineering', salary: 80000 },
{ name: 'Eve', dept: 'Marketing', salary: 70000 },
];
// Get Engineering salaries above 85k, sorted descending
const result = employees
.filter((e) => e.dept === 'Engineering')
.filter((e) => e.salary > 85000)
.sort((a, b) => b.salary - a.salary)
.map((e) => `${e.name}: $${e.salary.toLocaleString()}`);
console.log(result);
// ['Carol: $95,000', 'Alice: $90,000']
The callback you pass to array methods like map(), filter(), and reduce() is a closure - it captures variables from its surrounding scope. This lets you make your array operations configurable and reusable without hardcoding values inside the callback. A factory function that returns a callback is a common pattern you'll see in real codebases for exactly this reason.
// Closure captures `threshold` from outer scope
function makeThresholdFilter(threshold) {
return (value) => value >= threshold;
}
const numbers = [3, 7, 12, 1, 9, 5, 15];
const aboveFive = numbers.filter(makeThresholdFilter(5));
const aboveTen = numbers.filter(makeThresholdFilter(10));
console.log(aboveFive); // [7, 12, 9, 5, 15]
console.log(aboveTen); // [12, 15]
// Closure captures `multiplier`
function makeMultiplier(multiplier) {
return (n) => n * multiplier;
}
const tripled = numbers.map(makeMultiplier(3));
console.log(tripled); // [9, 21, 36, 3, 27, 15, 45]