EduFlow
JavaScript Fundamentals
0%

Section 3 · Working with Data

Objects, Destructuring and Spread

18 min3 min readCode playground

Lesson at a glance

Model real records as objects, read and update their properties, understand that objects are shared by reference, and copy, update and unpack them with spread and destructuring.

  1. 1Objects: named values together
  2. 2References: two names, one object
  3. 3Spread: copy and update without mutating
  4. 4Destructuring: unpacking into variables

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Until now an expense in Pocket was just a number. A real expense has several facts that belong together: what it was, how much, which category, when. Keeping those in separate arrays (labels[2], amounts[2], categories[2]) works until one array gets sorted and the others do not. You need one value that holds them all: an object.

Objects: named values together

An object is written with curly braces and contains properties, each a name and a value:

JavaScript
const expense = {
  id: "exp-1",
  label: "Coffee",
  amount: 450,
  category: "food",
};

console.log(expense.label);       // dot notation
console.log(expense["amount"]);   // bracket notation
console.log(expense.note);        // missing property: undefined

Dot notation is the everyday way to read a property. Brackets do the same job, but evaluate whatever is inside them, which you need when the property name is in a variable: const field = "category"; expense[field]. Reading a property that does not exist gives undefined, not an error.

You can add, change and remove properties after creation:

JavaScript
const expense = { label: "Coffee", amount: 450 };
expense.category = "food";   // add
expense.amount = 500;        // change
delete expense.category;     // remove
console.log(expense);
console.log(Object.keys(expense), Object.values(expense));

When a variable already has the name you want for a property, you can write it once. { label, amount } is shorthand for { label: label, amount: amount }, and you will see it constantly in real code:

JavaScript
const label = "Bus";
const amount = 270;
const expense = { label, amount, category: "travel" };
console.log(expense);

Object.keys and Object.values give you arrays of the names and the values, and Object.entries gives pairs, so for (const [key, value] of Object.entries(expense)) loops over every property.

Pocket’s data is now an array of objects, the most common shape of data in JavaScript, and everything from the last lesson still applies:

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, category: "food" },
  { id: "exp-2", label: "Train", amount: 1220, category: "travel" },
  { id: "exp-3", label: "Lunch", amount: 1350, category: "food" },
];
const food = expenses.filter((e) => e.category === "food");
console.log(food.map((e) => e.label));

References: two names, one object

Here is the most important thing to understand about objects and arrays. A variable does not hold the object; it holds a reference to it, like an address. Copying the variable copies the address:

JavaScript
const original = { label: "Coffee", amount: 450 };
const alias = original;
alias.amount = 999;
console.log(original.amount);       // 999
console.log(alias === original);    // true: the same object
console.log({ a: 1 } === { a: 1 }); // false: two different objects
The names original and alias both point to the same object, so a change through alias shows through original. The name copy points to a separate new object created with spread. original alias { label: "Coffee", amount: 999 } copy { label: "Coffee", amount: 450 } one object a new one
Assigning copies the reference; spread creates a new object

=== on objects compares references, not contents. Two objects that look identical are still two different objects.

This matters because functions receive references too. If a function changes an object it was given, the caller’s object changes:

JavaScript
function applyDiscount(expense) {
  expense.amount = Math.round(expense.amount * 0.9);
  return expense;
}

const lunch = { label: "Lunch", amount: 1350 };
const discounted = applyDiscount(lunch);
console.log(discounted.amount, lunch.amount); // both 1215

The function looks as if it returns a discounted version, but it quietly rewrote the original as well. Somewhere else, a total that included lunch is now wrong, and nothing on screen says why. That is how bugs like “I only displayed the expenses, why did their amounts change?” happen. The fix is to build a new object instead of changing the one you were given, and spread makes that easy.

Spread: copy and update without mutating

Spread syntax, three dots, copies all the properties of an object (or items of an array) into a new one:

JavaScript
const expense = { id: "exp-1", label: "Coffee", amount: 450 };
const updated = { ...expense, amount: 500 };
console.log(expense.amount, updated.amount);

const list = [expense];
const longer = [...list, { id: "exp-2", label: "Bus", amount: 270 }];
console.log(list.length, longer.length);

In { ...expense, amount: 500 }, properties are copied in order and the later amount overwrites the copied one. You get a new object, and the original is untouched. Combined with map, this is the standard way to change one item in a list without mutating anything:

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450 },
  { id: "exp-2", label: "Bus", amount: 270 },
];
const fixed = expenses.map((e) => (e.id === "exp-2" ? { ...e, amount: 300 } : e));
console.log(fixed[1].amount, expenses[1].amount);

Destructuring: unpacking into variables

Destructuring pulls properties out of an object into variables of the same name:

JavaScript
const expense = { label: "Train", amount: 1220, category: "travel" };
const { label, amount, note = "no note" } = expense;
console.log(label, amount, note);

const [first, second] = ["food", "travel", "fun"];
console.log(first, second);

The = "no note" is a default, used when the property is missing or undefined. For arrays, destructuring goes by position instead of name.

Destructuring is most useful in function parameters, where it documents exactly which properties a function needs:

JavaScript
function describe({ label, amount, category = "other" }) {
  return `${label}: ${(amount / 100).toFixed(2)} (${category})`;
}
console.log(describe({ label: "Train", amount: 1220 }));

Compare it with the version without destructuring, which repeats expense. on every line and hides which properties matter until you read the whole body. With destructuring, the first line of the function is its contract: give me something with a label, an amount, and optionally a category. Callers can pass a full expense object with ten other properties; the function simply ignores the rest.

The ... also works in reverse, as rest: const { id, ...details } = expense puts id in its own variable and every other property in a new object called details. It is a neat way to drop a property without mutating the original: everything you did not name ends up in the new object.

Pocket now has realistic data. Next you summarise it: totals per category, and lists sorted by amount.

Key takeaways

  • An object groups named values (properties); read them with dot notation, or with brackets when the property name is in a variable.
  • Objects and arrays are shared by reference, so two names can point at the same object and a change through one is visible through the other.
  • Spread ({ ...expense, amount: 500 }, [...list, item]) makes a new, shallow copy, which is how you update data without mutating it.
  • Destructuring (const { label, amount } = expense) pulls properties into variables, and works in function parameters with defaults.

Further reading

Check your understanding

4 questions

Question 1 of 4

What does this log?

JavaScript
const a = { label: "Coffee", amount: 450 };
const b = a;
b.amount = 500;
console.log(a.amount);
Question 2 of 4

const updated = { ...expense, amount: 500 }; What is true afterwards?

Question 3 of 4

Given const expense = { label: "Bus", amount: 270 };, what does const { label, category = "other" } = expense; give you?

Question 4 of 4

You have const key = "category";. How do you read expense.category using key?

Practice

Code playground

Update expenses without mutating them

The starter’s functions change the arrays and objects they are given, so the caller’s data changes behind its back. Rewrite them to return new values instead:

  • addExpense(list, expense) returns a new array with expense at the end.
  • updateAmount(list, id, amount) returns a new array in which the expense with that id is a new object with the new amount. Other expenses stay as they are, and nothing in list changes.
  • describe(expense) returns a string like "Train: $12.20 (travel)", using "other" when there is no category. Destructure the parameter.
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, category: "food" },
  { id: "exp-2", label: "Train", amount: 1220, category: "travel" },
];

// Return a NEW array with the expense added at the end
function addExpense(list, expense) {
  list.push(expense);
  return list;
}

// Return a NEW array where the expense with this id has the new amount
function updateAmount(list, id, amount) {
  const found = list.find((e) => e.id === id);
  found.amount = amount;
  return list;
}

// "Train: $12.20 (travel)"; category defaults to "other"
function describe(expense) {
  return expense.label;
}

console.log(describe(expenses[1]));
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • addExpense returns the list with the new expense at the end
  • addExpense leaves the original array unchanged
  • updateAmount changes the amount of the matching expense
  • updateAmount leaves the original array and objects unchanged
  • describe formats the expense, defaulting the category to “other”