Warm-up · Activity 1 of 7
Warm-up: what does this print?
const book = { title: "Dune", year: 1965 };
console.log(book.title, book.pages);// B5.3 · ~30 min · Beginner
After this lesson you build and change objects, unpack them with destructuring, copy and merge them with spread, and read nested values safely.
You will be able to
Warm-up · Activity 1 of 7
const book = { title: "Dune", year: 1965 };
console.log(book.title, book.pages);Predict · Activity 2 of 7
const a = { size: "M", colour: "red" };
const b = { ...a, size: "L" };
console.log(a.size, b.size, b.colour);Practice · Activity 3 of 7
const item = { price: 4, name: "tea" };
const key = "price";
console.log(item[____]);Practice · Activity 4 of 7
Practice · Activity 5 of 7
const stock = { apples: 3, pears: 0 };
console.log(Object.keys(stock), Object.values(stock));Brain teaser · Activity 6 of 7
const original = { name: "Ada", address: { city: "Berlin" } };
const copy = { ...original };
copy.name = "Grace";
copy.address.city = "Paris";
console.log(original);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
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.jsOutput
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 ParisTab 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.
Exercise 1 of 2
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.
Run the tests: the label ends in undefined, because the product has no currency key.
Unpack the parameter with a default: function label({ name, price, currency = "EUR" }).
user.address is undefined for Bo. user.address?.city gives undefined instead of an error, and ?? "unknown" replaces it.
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";
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
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.
Run the tests: after withQuantity(item, 5) the original item has quantity 5. Assigning to item changes the caller's object.
Build a new object instead: { ...item, quantity }. The later key wins, and quantity is shorthand for quantity: quantity.
{ ...cart } is shallow: the copy shares cart.items. Give it its own array: { ...cart, items: [...cart.items] }.
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] };
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsconst prices = { tea: 3, milk: 1.2 };
for (const price of prices) {
console.log(price);
}
What Node.js prints
TypeError: prices is not iterableWhy, 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)) { … }.
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" } = {}.
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.