Skip to content
aviral gupta

// B5.3 · ~30 min · Beginner

Objects, destructuring and spread

After this lesson you build and change objects, unpack them with destructuring, copy and merge them with spread, and read nested values safely.

Lesson 3 of 5 in B5 Arrays, objects and JSON

You will be able to

  • Read, add, change and delete an object's keys, check them with in and Object.hasOwn, and loop over them with Object.entries
  • Unpack arrays and objects with destructuring, with defaults and new names, also in function parameters
  • Copy and merge objects with spread, know that the copy is shallow, and read nested values with ?.
  1. Warm-up · Activity 1 of 7

    Warm-up: what does this print?

    const book = { title: "Dune", year: 1965 };
    console.log(book.title, book.pages);
  2. Predict · Activity 2 of 7

    Predict before you read on: what does this print?

    const a = { size: "M", colour: "red" };
    const b = { ...a, size: "L" };
    console.log(a.size, b.size, b.colour);
  3. Practice · Activity 3 of 7

    The key to read is in the variable key. Fill in the gap so that the line reads the key that key names, whichever it is.

    const item = { price: 4, name: "tea" };
    const key = "price";
    console.log(item[____]);
    console.log(item[]);
  4. Practice · Activity 4 of 7

    user is { name: "Ada" } and list is [5, 6, 7]. Match each line to what it creates.

  5. Practice · Activity 5 of 7

    What does this print?

    const stock = { apples: 3, pears: 0 };
    console.log(Object.keys(stock), Object.values(stock));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The program changes only the copy. What does it print?

    const original = { name: "Ada", address: { city: "Berlin" } };
    const copy = { ...original };
    copy.name = "Grace";
    copy.address.city = "Paris";
    console.log(original);
  7. Apply · Activity 7 of 7

    Mini-task: a contact card. Create an object with a name, an email and a phone number. Add a city, change the email and delete the phone number, then print every key and value with Object.entries and for...of. Destructure name and a company with the default "none". Make an updated copy with spread and a new city, and print both cities to show the original kept its own. Finally read a street from a missing address with ?..

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A user profile

One profile object, used in all the ways of this lesson: it is read with a dot and with brackets, changed, checked with in and Object.hasOwn, walked with Object.entries, unpacked with a rename and a default, and merged with spread. The last part shows the shallow copy: the copy's new name stays in the copy, but its new city reaches the original.

main.js

// A user profile: read, change, loop, unpack, copy and merge.
const user = { name: "Ada", city: "Berlin", tags: ["admin"] };
console.log(user.name, user["city"], user.age);

const key = "city";
user[key] = "Munich";
user.age = 36;
delete user.tags;
console.log(user);
console.log("age" in user, Object.hasOwn(user, "tags"));

for (const [k, v] of Object.entries(user)) {
  console.log(`${k} = ${v}`);
}

const { name, age: years, role = "guest" } = user;
console.log(name, years, role);

const defaults = { theme: "light", lang: "en" };
const settings = { ...defaults, lang: "de" };
console.log(settings, defaults);

const original = { name: "Ada", address: { city: "Berlin" } };
const copy = { ...original };
copy.name = "Grace";
copy.address.city = "Paris";
console.log(original);
console.log(original.phone?.mobile, original.address?.city);

Run it with

node main.js

Output

Ada Berlin undefined
{ name: 'Ada', city: 'Munich', age: 36 }
true false
name = Ada
city = Munich
age = 36
Ada 36 guest
{ theme: 'light', lang: 'de' } { theme: 'light', lang: 'en' }
{ name: 'Ada', address: { city: 'Paris' } }
undefined Paris
  • user.age is undefined at first, because the key does not exist yet.
  • user[key] with key = "city" changes the city key.
  • role has no key in user, so it gets its default, "guest"; age is renamed to years.
  • settings has lang "de" from the later key, and defaults is unchanged.
  • original.address.city became Paris through the copy, because address is shared.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Read a product and a user safely

label(product) should return the name, a colon, the price and the currency, such as Tea: 3 EUR. When the product has no currency, use EUR. cityOf(user) should return the user's city, user.address.city, or "unknown" when the user has no address. The starter prints undefined for a missing currency and stops with a TypeError for a missing address. Fix both: destructuring with a default helps with the first, ?. and ?? with the second.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the label ends in undefined, because the product has no currency key.

  2. Hint 2

    Unpack the parameter with a default: function label({ name, price, currency = "EUR" }).

  3. Hint 3

    user.address is undefined for Bo. user.address?.city gives undefined instead of an error, and ?? "unknown" replaces it.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// "Tea: 3 EUR"; the currency is EUR when the product has none.
export function label({ name, price, currency = "EUR" }) {
  return name + ": " + price + " " + currency;
}

// The user's city, or "unknown" when there is no address.
export function cityOf(user) {
  return user.address?.city ?? "unknown";
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// "Tea: 3 EUR"; the currency is EUR when the product has none.
export function label(product) {
  return product.name + ": " + product.price + " " + product.currency;
}

// The user's city, or "unknown" when there is no address.
export function cityOf(user) {
  return user.address.city;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {label, cityOf} from './main.js';

test('label uses the currency of the product', () => {
  const got = label({ name: 'Tea', price: 3, currency: 'USD' });
  assert.equal(got, 'Tea: 3 USD', `label returned ${JSON.stringify(got)}, expected "Tea: 3 USD"`);
});

test('label uses EUR when the product has no currency', () => {
  const got = label({ name: 'Milk', price: 1.2 });
  assert.equal(got, 'Milk: 1.2 EUR', `label returned ${JSON.stringify(got)}, expected "Milk: 1.2 EUR"`);
});

test('cityOf reads the city from the address', () => {
  const got = cityOf({ name: 'Ada', address: { city: 'Berlin' } });
  assert.equal(got, 'Berlin', `cityOf returned ${JSON.stringify(got)}, expected "Berlin"`);
});

test('cityOf gives unknown when there is no address', () => {
  const got = cityOf({ name: 'Bo' });
  assert.equal(got, 'unknown', `cityOf returned ${JSON.stringify(got)}, expected "unknown"`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Change a copy, not the original

withQuantity(item, quantity) should return a new object with every key of item and the given quantity, and leave item unchanged. copyCart(cart) should return a copy of a cart, { owner, items }, that can get new items without changing the original cart. The starter changes item itself, and its cart copy shares the items array. Fix both with spread.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: after withQuantity(item, 5) the original item has quantity 5. Assigning to item changes the caller's object.

  2. Hint 2

    Build a new object instead: { ...item, quantity }. The later key wins, and quantity is shorthand for quantity: quantity.

  3. Hint 3

    { ...cart } is shallow: the copy shares cart.items. Give it its own array: { ...cart, items: [...cart.items] }.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// A new object: item with this quantity. item stays unchanged.
export function withQuantity(item, quantity) {
  return { ...item, quantity };
}

// A copy of the cart whose items can change on their own.
export function copyCart(cart) {
  return { ...cart, items: [...cart.items] };
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// A new object: item with this quantity. item stays unchanged.
export function withQuantity(item, quantity) {
  item.quantity = quantity;
  return item;
}

// A copy of the cart whose items can change on their own.
export function copyCart(cart) {
  return { ...cart };
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {withQuantity, copyCart} from './main.js';

test('withQuantity returns the item with the new quantity', () => {
  const got = withQuantity({ name: 'Tea', quantity: 1 }, 3);
  assert.deepEqual(got, { name: 'Tea', quantity: 3 }, `withQuantity returned ${JSON.stringify(got)}, expected {"name":"Tea","quantity":3}`);
});

test('withQuantity leaves the original item unchanged', () => {
  const item = { name: 'Tea', quantity: 1 };
  withQuantity(item, 5);
  assert.equal(item.quantity, 1, `the original item's quantity became ${item.quantity}, expected it to stay 1`);
});

test('copyCart copies the owner and the items', () => {
  const got = copyCart({ owner: 'Ada', items: ['tea'] });
  assert.deepEqual(got, { owner: 'Ada', items: ['tea'] }, `copyCart returned ${JSON.stringify(got)}`);
});

test('Adding to the copy leaves the original cart alone', () => {
  const cart = { owner: 'Ada', items: ['tea'] };
  const copy = copyCart(cart);
  copy.items.push('milk');
  assert.deepEqual(cart.items, ['tea'], `the original cart's items became ${JSON.stringify(cart.items)}, expected ["tea"]`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

for...of over an object

const prices = { tea: 3, milk: 1.2 };
for (const price of prices) {
  console.log(price);
}

What Node.js prints

TypeError: prices is not iterable

Why, and the fix

for...of walks arrays and strings, but a plain object is not iterable. Turn it into an array first: Object.values(prices) for the values, Object.keys(prices) for the names, or Object.entries(prices) for both: for (const [name, price] of Object.entries(prices)) { … }.

Destructuring a parameter that was not passed

function greet({ name }) {
  return "Hello, " + name;
}
console.log(greet());

What Node.js prints

TypeError: Cannot destructure property 'name' of 'undefined' as it is undefined.

Why, and the fix

greet unpacks its first argument, but the call passes none, so it tries to read name from undefined. Pass an object, greet({ name: "Ada" }), or give the parameter a default object: function greet({ name } = {}) { … }; name is then undefined instead of an error, and can have its own default: { name = "friend" } = {}.

Reading through a key that is missing

const user = { name: "Ada" };
console.log(user.address.city);

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'city')

Why, and the fix

user has no address, so user.address is undefined, and reading city from undefined is a TypeError. The message names the key it tried to read. When a part may be missing, use optional chaining: user.address?.city gives undefined. Add ?? for a fallback: user.address?.city ?? "unknown".

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Named values in one place

An object literal groups values under keys: const user = { name: "Ada", city: "Berlin" };. Read a key with a dot, user.name, or with brackets and a string, user["city"]; brackets also take a variable, user[key]. A key that does not exist gives undefined, not an error. Assigning adds or changes a key: user.age = 36. delete user.city removes one. "age" in user and Object.hasOwn(user, "age") tell you whether the key is there. Object.keys, Object.values and Object.entries give arrays, so for (const [k, v] of Object.entries(user)) walks an object; for...of on the object itself fails. Shorthand { name, age } means { name: name, age: age }.

Destructuring unpacks values

const { name, city } = user; creates two constants from the keys of the same names. Rename with a colon, { age: years }, and give a default with =, { role = "guest" }: the default is used when the key is missing or undefined. Arrays unpack by position: const [first, second] = list;, and a gap skips an item: const [, second] = list;. Destructuring works in parameters too: function label({ name, price }) { … } takes one object and unpacks it. Called with no argument at all, it stops with TypeError: Cannot destructure property 'name' of 'undefined'; a default, ({ name } = {}), prevents that.

Spread copies one level, and ?. reads safely

Spread copies an object's keys into a new one: { ...defaults }. Merge by spreading several, and later keys win: { ...defaults, lang: "de" } keeps every default but lang. The copy is shallow: a nested object or array is not copied, both objects share it, so copy.address.city = "Paris" also changes original.address. Assigning an object, const b = a;, copies nothing at all; a and b are one object. Copy nested parts yourself, as in { ...cart, items: [...cart.items] }. Optional chaining stops at undefined or null: user.address?.city gives undefined where user.address.city would throw a TypeError.

Sources

Last reviewed October 3, 2026