Arrays and Objects
Creating, reading, updating, copying, and transforming the two structures nearly every JavaScript program is built from.
By the end of this lesson, you can
- Distinguish mutating array operations from non-mutating ones, and explain why the difference matters when copying data
- Use destructuring and spread to extract and copy values from arrays and objects concisely
- Choose the right array method — map, filter, find, some, every, or reduce — for a given transformation
- Safely read a value from nested data without crashing on a missing property
Why it matters
Arrays and plain objects are how nearly every JavaScript program represents real data — a list of
tasks, a user record, a shopping cart. Recall from Values, Variables, and
Operators that both are objects, compared by
identity rather than content — which means copying them correctly takes a deliberate operation, not
just =.
Creating, accessing, and updating
const fruits = ['apple', 'banana', 'cherry'];
console.log(fruits[0]); // "apple"
console.log(fruits.length); // 3
const user = { name: 'Ada', role: 'engineer' };
console.log(user.name); // "Ada"
console.log(user['role']); // "engineer" — bracket syntax, useful for a computed key
user.role = 'senior engineer'; // update an existing property
user.location = 'London'; // add a new one
Mutating versus non-mutating operations
Some array operations change the array in place; others return a new array and leave the original untouched. Both categories are common, and mixing them up is a frequent source of bugs — especially once a reference to the same array is shared in more than one place.
| Method | Mutates original? | Returns |
|---|---|---|
| push(x) / pop() | Yes | The new length / the removed element |
| sort() / reverse() | Yes | The same array, now reordered |
| slice(start, end) | No | A new array — a shallow copy of the requested range |
| map(fn) / filter(fn) | No | A new array |
| concat(other) | No | A new array |
Destructuring and spread
Destructuring pulls values out of an array or object into their own bindings in one step.
Spread (...) expands an array or object’s contents into a new array, object, or argument
list — the most common way to copy or combine data:
const point = { x: 10, y: 20, z: 30 };
const { x, y } = point; // destructuring — pulls out x and y only
console.log(x, y); // 10 20
const original = [1, 2, 3];
const copy = [...original, 4]; // spread — new array, original untouched
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
const settings = { theme: 'dark' };
const merged = { ...settings, fontSize: 16 }; // spread into a new object
console.log(merged); // { theme: "dark", fontSize: 16 }The rest parameter, also written ..., is the mirror image — it gathers remaining items
instead of spreading them out, most often seen collecting extra function arguments or the “leftover”
part of a destructure:
function sum(first, ...rest) {
return rest.reduce((total, n) => total + n, first);
}
console.log(sum(1, 2, 3, 4)); // 10 — first = 1, rest = [2, 3, 4]
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
Essential array methods
map transforms every element into something new; filter keeps only the elements matching a
condition; find returns the first match (or undefined); some/every test whether any/all
elements match:
const prices = [12.5, 8, 25, 3.75];
const withTax = prices.map((p) => p * 1.08);
const expensive = prices.filter((p) => p > 10);
const firstExpensive = prices.find((p) => p > 10);
const anyOverBudget = prices.some((p) => p > 20);
const allAffordable = prices.every((p) => p < 30);
reduce is the most general — it combines every element into a single accumulated result, and
everything map/filter do could technically be written with reduce (though the specific methods
read more clearly when they fit):
const cart = [
{ name: 'Book', price: 12.5, qty: 2 },
{ name: 'Pen', price: 1.5, qty: 3 },
];
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log(total); // 29.5reduce’s first argument is a function (accumulator, currentElement) => newAccumulator; the
second argument (0 above) is the accumulator’s starting value.
Nested data and safe access
Real data is often nested several levels deep, and a level might legitimately be missing:
const order = { customer: { name: 'Ada' } }; // no shippingAddress yet
const city = order.customer.shippingAddress.city;
// TypeError: Cannot read properties of undefined (reading 'city')
const safeCity = order.customer.shippingAddress?.city ?? 'Unknown';
// "Unknown" — ?. short-circuits at the missing link, ?? supplies the fallback
JSON: parse and stringify
JSON.stringify converts a JavaScript value into a JSON text string; JSON.parse does the reverse.
This is how data crosses boundaries — saved to localStorage, sent over fetch — since both only
handle text.
const data = { name: 'Ada', active: true };
const json = JSON.stringify(data); // '{"name":"Ada","active":true}'
const restored = JSON.parse(json); // a new object, equal in content, not identity
Check your understanding
Or reveal the answer without checking
Answer:[1, 2, 3, 4]
const copy = original; doesn't copy the array — it copies the reference. copy and original point at the same array, so mutating one through push() is visible through the other.
Or reveal the answer without checking
Answer:prices.filter((p) => p > 5)
filter() always returns a new array and leaves the original untouched. sort(), push(), and splice() all mutate the array they're called on.
Practice: warm-up
Given const inventory = [{ item: 'pen', qty: 12 }, { item: 'notebook', qty: 0 }, { item: 'stapler', qty: 3 }];,
predict the result of each before checking: inventory.filter((i) => i.qty > 0),
inventory.some((i) => i.qty === 0), inventory.map((i) => i.item).
Stuck? Reveal one hint at a time.
Hint 1
filter keeps elements where the callback returns true, discarding the rest — the result is always the same length or shorter.
Hint 2
some returns a single boolean: true if at least one element matches.
Hint 3
map returns an array the same length as the original, one transformed value per element.
Reveal the results
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
inventory.filter((i) => i.qty > 0)
-> [{ item: "pen", qty: 12 }, { item: "stapler", qty: 3 }]
inventory.some((i) => i.qty === 0)
-> true
inventory.map((i) => i.item)
-> ["pen", "notebook", "stapler"]Practice: apply it
Given the same inventory array from above, write summarize(inventory) that returns an object
{ totalItems, inStockCount, outOfStockNames } — the sum of all qty, the count of items with
qty > 0, and an array of item names where qty === 0.
Stuck? Reveal one hint at a time.
Hint 1
totalItems is a single accumulated number across all items — reduce fits naturally.
Hint 2
inStockCount and outOfStockNames both start from filtering, then take a length or a map.
Hint 3
You can compute each of the three independently and assemble them into one returned object at the end.
Reveal a reference solution
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
function summarize(inventory) {
const totalItems = inventory.reduce((sum, i) => sum + i.qty, 0);
const inStockCount = inventory.filter((i) => i.qty > 0).length;
const outOfStockNames = inventory.filter((i) => i.qty === 0).map((i) => i.item);
return { totalItems, inStockCount, outOfStockNames };
}Summary
- Arrays and objects are compared by identity —
const copy = original;copies the reference, not the data, so mutating one throughpush/splice/property assignment is visible through the other. - Some array methods mutate in place (
push,pop,sort,splice); others return a new array and leave the original untouched (map,filter,slice,concat). - Destructuring pulls values out; spread (
...) copies or combines; rest (also...) gathers leftover values — all shallow, so nested objects/arrays are still shared between original and copy. maptransforms,filterselects,find/some/everytest, andreducecombines everything into one accumulated result.?.and??together make reading a possibly-missing nested property safe.JSON.stringify/JSON.parsecross text boundaries (storage, network) but dropundefinedand functions, and can’t representNaN,Infinity, or liveDateobjects.