BeginnerArrays and objects~40 min

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.

Selected array methods, grouped by whether they mutate
MethodMutates original?Returns
push(x) / pop()YesThe new length / the removed element
sort() / reverse()YesThe same array, now reordered
slice(start, end)NoA new array — a shallow copy of the requested range
map(fn) / filter(fn)NoA new array
concat(other)NoA 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:

JavaScriptDestructuring and spread together
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):

JavaScriptreduce, building up one value from many
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.5

reduce’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

What does this log? const original = [1, 2, 3]; const copy = original; copy.push(4); console.log(original);
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.

Which array method call does NOT mutate the original array?
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.

  1. Hint 1

    filter keeps elements where the callback returns true, discarding the rest — the result is always the same length or shorter.

  2. Hint 2

    some returns a single boolean: true if at least one element matches.

  3. 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.

  1. Hint 1

    totalItems is a single accumulated number across all items — reduce fits naturally.

  2. Hint 2

    inStockCount and outOfStockNames both start from filtering, then take a length or a map.

  3. 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 through push/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.
  • map transforms, filter selects, find/some/every test, and reduce combines everything into one accumulated result.
  • ?. and ?? together make reading a possibly-missing nested property safe.
  • JSON.stringify/JSON.parse cross text boundaries (storage, network) but drop undefined and functions, and can’t represent NaN, Infinity, or live Date objects.