JavaScript 03 · JavaScript in general
Objects
Dot vs. square brackets, destructuring, spread, Object.entries and dynamic keys.
Loading lesson…
JavaScript 03 · JavaScript in general
Dot vs. square brackets, destructuring, spread, Object.entries and dynamic keys.
Loading lesson…
Picture the card catalogue in a library. Every card describes one book and has labelled fields: title, author, year. An object in JavaScript is exactly such a card – a set of named fields (properties), each with a value.
const book = {title: 'The Little Prince', // key: valueauthor: 'Saint-Exupéry',year: 1943,available: true,}
Objects are everywhere: a user, a product in the cart, the app's settings, a response from the server. In React, the props a component receives are an object too. If you can handle objects well, you write shorter and more readable code.
You can read a field of the card in two ways: with a dot, or with square brackets holding the field's name as text in quotes.
const user = { name: 'Anna', city: 'London', 'favorite color': 'blue' }user.name // 'Anna' – you write the property name after the dotuser['name'] // 'Anna' – the same, only the name is text in bracketsuser['favorite color'] // 'blue' – a name with a space can only go in bracketsuser.favorite color // ❌ error – a name after the dot can't contain a space
Square brackets can do something the dot can't: they don't need a fixed text inside. They can hold a variable – or any expression. JavaScript evaluates it first and only then uses the resulting text as the property name.
Imagine that at the library you get a slip of paper saying “year” and you are to read the field written on the slip. You don't look for a field called “slip” – you look at what the slip says and read the field year. A variable in square brackets is such a slip:
const field = 'city' // the variable field holds the TEXT 'city'user[field] // 'London' – JavaScript fills in the value: user['city']const whatever = 'name' // the variable can have any nameuser[whatever] // 'Anna' – user['name']user['ci' + 'ty'] // 'London' – the expression is worked out to 'city' first
Watch out for quotes – just add them and a completely different property is looked up:
| Syntax | What JavaScript does | Result |
|---|---|---|
✅ user[field] | takes the value of the variable field ('city') and reads user.city | 'London' |
❌ user['field'] | looks for a property literally called “field” – user has no such property | undefined |
❌ user.field | the same as user['field'] | undefined |
What is it good for? For example, when you write a function that should be able to read any property – which one is decided only by whoever calls it:
const read = (obj, field) => obj[field]read(user, 'name') // 'Anna'read(user, 'city') // 'London'// with obj.field both calls would return undefined – it would look for a property "field"
So when do you use which syntax?
| Syntax | When |
|---|---|
obj.name | You know in advance which property you want – that is most cases. |
obj[key] | Which property to read is stored in a variable – say in a function parameter or while looping over an object. |
const order = {id: 7,customer: { name: 'Peter', address: { city: 'Boston' } },}order.customer.address.city // 'Boston' – read nested objects dot after dotorder.note // undefined – a property that doesn't exist isn't an errororder.delivery.date // 💥 TypeError – delivery is missing (it's undefined) and you can't read .date from undefinedorder.delivery?.date // undefined – ?. sees that delivery is missing and doesn't read .date at all (more in lesson JavaScript 06)
const user = { name: 'Anna' }user.age = 31 // adds a new propertyuser.name = 'Anna S.' // changes an existing oneuser['city'] = 'London' // the same with bracketsdelete user.age // deletes a propertyuser // { name: 'Anna S.', city: 'London' }
Three ways of writing that save you work when creating objects and that you will see in code all the time: a property from a variable of the same name, methods and computed keys.
When a variable has the same name as the property you want to create, you don't have to write the name twice:
const name = 'Anna'const age = 31const user = { name: name, age: age } // long: the property name gets the value of the variable nameconst user = { name, age } // short: the same// both create { name: 'Anna', age: 31 }
A property of an object can hold a function too – then it's called a method. The object counter below has two properties: the number count and the method increment, which increases that number. A method has a shorter form too:
// long: a property increment whose value is a functionconst counter = {count: 0,increment: function () {this.count++ // this = the object you call the method on (here counter)},}// short: the same, just without ": function"const counter = {count: 0,increment() {this.count++},}counter.increment() // you call the method through the object and a dotcounter.count // 1
When you don't know the property name in advance because it's stored in a variable, put it in square brackets when creating the object:
const field = 'email'const value = 'anna@example.com'const update = { [field]: value } // { email: 'anna@example.com' }const wrong = { field: value } // { field: 'anna@example.com' } – literally "field"// Typical use: one function for all the fields of a formfunction updateForm(form, field, value) {return { ...form, [field]: value }}updateForm({ name: '', email: '' }, 'name', 'Anna') // { name: 'Anna', email: '' }const prefix = 'color'const theme = { [`${prefix}Primary`]: 'blue' } // { colorPrimary: 'blue' }
Destructuring is unpacking a box: instead of "take the box and from it the thing labelled name" you say straight away "I want the things name and city".
const user = { name: 'Anna', city: 'London', age: 31 }const { name, city } = user // two variables at onceconst { name: userName } = user // renaming: the variable will be called userNameconst { role = 'guest' } = user // a default value when the property is missing (or undefined)const { name: n, role: r = 'guest' } = user // renaming and a default valueconst { age, ...rest } = user // ... collects the remaining properties: rest = { name: 'Anna', city: 'London' }// (rest is an ordinary variable name, it could be ...others too)
const order = { id: 7, customer: { name: 'Peter', address: { city: 'Boston' } } }const { customer: { name, address: { city } } } = ordername // 'Peter'city // 'Boston'// customer and address weren't created as variables – we just "passed through" them
// The function gets an object and unpacks what it needs right awayfunction formatUser({ name, city = 'unknown' }) {return `${name} (${city})`}formatUser({ name: 'Anna', city: 'London', age: 31 }) // 'Anna (London)'// This is exactly what a React component looks like:function UserCard({ name, city }) { … }// It works in a callback too. map calls a function for every item of an array (more in lesson JavaScript 04)// and here every item is a user object – the parameter unpacks name from it right away:const users = [{ name: 'Anna', city: 'London' }, { name: 'Peter', city: 'Leeds' }]users.map((user) => user.name) // without destructuring: ['Anna', 'Peter']users.map(({ name }) => name) // with destructuring: the same
An array isn't unpacked by name, but by position: the first variable gets the first item, the second one the second item and so on. That's why you choose the variable names yourself.
const [first, second] = ['apple', 'pear', 'plum']// first = 'apple', second = 'pear' (plum is left over and isn't stored anywhere)const [, , third] = ['apple', 'pear', 'plum']// commas without a name skip the first two items: third = 'plum'const [head, ...tail] = [1, 2, 3, 4]// head = 1, tail = [2, 3, 4] – ... collects the rest, just like with an object
Why you need it: in React you will write this line in almost every component. The function useState returns an array of two items – the current value and a function that changes it – and destructuring names them right away:
const [count, setCount] = useState(0)// useState(0) returns the array [0, function]// count = 0 (the first item), setCount = the function that changes it (the second item)const [name, setName] = useState('Anna') // you choose the names, the position decides
What exactly useState does you'll learn in the lesson on state. For now it's enough to recognise it as ordinary array destructuring.
Three dots before an object mean "pour all its properties in here".
const user = { name: 'Anna', city: 'London' }const copy = { ...user } // a shallow copy: a new object, the same propertiescopy === user // false – they are two different objectsconst moved = { ...user, city: 'Boston' } // a copy with an overwritten propertyconst withAge = { ...user, age: 31 } // a copy with a new propertyconst defaults = { theme: 'light', lang: 'en', fontSize: 16 }const saved = { theme: 'dark' }const settings = { ...defaults, ...saved } // { theme: 'dark', lang: 'en', fontSize: 16 }
Sometimes an object should get a property only under a certain condition – say the role property only for an administrator. You build it from two things: spread, which you already know, and an “either, or” choice.
The syntax condition ? A : B means “if the condition holds, take A, otherwise B” (lesson JavaScript 06 covers it in detail). Here it chooses between an object with the role property and an empty object – and spread then pours whatever came out into the new object:
const isAdmin = trueconst user = {name: 'Anna',...(isAdmin ? { role: 'admin' } : {}),}// step by step, when isAdmin is true:// 1. the parentheses choose { role: 'admin' }// 2. ... pours it into the object → { name: 'Anna', role: 'admin' }// and when isAdmin is false:// 1. the parentheses choose {} – an empty object// 2. ... has nothing to pour → { name: 'Anna' } – the role property isn't created at all
Why not simply role: isAdmin ? 'admin' : undefined? Then the role property would always be created, just with the value undefined for other users. With spread, other users don't have it at all.
For going through an object JavaScript has three ready-made functions: Object.keys, Object.values and Object.entries. Unlike rest or field, which you named yourself, these names are fixed – you have to write them exactly like this, including the capital O in Object. Each of them returns an array (arrays are covered in detail in the next lesson):
| Function | Returns |
|---|---|
Object.keys(obj) | an array of the property names (keys) |
Object.values(obj) | an array of the values |
Object.entries(obj) | an array of [name, value] pairs (entries) |
const prices = { apple: 12, pear: 15, plum: 8 }Object.keys(prices) // ['apple', 'pear', 'plum']Object.values(prices) // [12, 15, 8]Object.entries(prices) // [['apple', 12], ['pear', 15], ['plum', 8]]
Most often you go through entries with a for…of loop. Each pair is destructured right away into two variables – and you choose their names (fruit, price) yourself again:
for (const [fruit, price] of Object.entries(prices)) {console.log(`${fruit} costs ${price} cents`)}// apple costs 12 cents// pear costs 15 cents// plum costs 8 cents
A few more handy things:
Object.keys(prices).length // 3 – the number of propertiesObject.keys({}).length === 0 // true – that's how to recognise an empty object'apple' in prices // true – does the object have the property 'apple'?Object.hasOwn(prices, 'banana') // false – the same another way: does it have 'banana'?Object.fromEntries([['a', 1], ['b', 2]]) // { a: 1, b: 2 } – the opposite of entries: pairs back to an object
When you want to change all the values of an object, go through entries and build the result in a new object. Array methods like map and filter come in the next lesson; then you'll be able to write it more briefly.
// Raise every price by 10 %const result = {}for (const [fruit, price] of Object.entries(prices)) {result[fruit] = Math.round(price * 1.1) // the property name is in the variable fruit → brackets}// result = { apple: 13, pear: 17, plum: 9 }
What you see: The user object and buttons with operations. For each one you see the code, an explanation and the result.
Try it:
city was renamed to town and the missing email got a default value.The takeaway: An object can be read with a dot or with brackets, unpacked with destructuring, copied with spread and turned into an array with Object.entries.
Loading the interactive part…
So far every object described one thing – a user, an order, a book. But an object can also serve as a dictionary. In a dictionary you find the word you're looking for on the left and read its meaning on the right. In an object the left side is the key (what you look up) and the right side is the value (what you get) – and you look up with square brackets, because the word you're looking for is in a variable.
Picture an online shop: the order status is stored as a short code ('new', 'paid', 'shipped') and on the page you want to show a readable label. The dictionary for it looks like this:
const STATUS_LABELS = {new: 'New order', // key 'new' → value 'New order'paid: 'Paid',shipped: 'On its way',}const status = 'paid' // the order status, say from the serverSTATUS_LABELS[status] // 'Paid' – finds the key 'paid' and returns its valueSTATUS_LABELS['lost'] // undefined – there's no such key in the dictionary
The name in capitals is just a habit: it says this is a fixed table that doesn't change while the program runs. So that the function doesn't return undefined for an unknown status, you add ?? – “if there's nothing on the left, take what's on the right” (more in lesson JavaScript 06). Compare it with a chain of conditions doing the same:
// ❌ A chain of conditions – every new status means another line with iffunction statusLabel(status) {if (status === 'new') return 'New order'if (status === 'paid') return 'Paid'if (status === 'shipped') return 'On its way'return 'Unknown status'}// ✅ A dictionary – a new status = a new line in the dictionary, the function stays the sameconst statusLabel = (status) => STATUS_LABELS[status] ?? 'Unknown status'statusLabel('paid') // 'Paid'statusLabel('lost') // 'Unknown status'
A dictionary can also be filled as you go. You want to count votes: the key will be the answer, the value the count. You start with an empty object and add one for every vote:
const votes = ['yes', 'no', 'yes', 'yes']const counts = {}for (const v of votes) {counts[v] = (counts[v] ?? 0) + 1// (counts[v] ?? 0) = the count so far, or 0 when this answer isn't in the dictionary yet}// step by step:// 'yes' → counts['yes'] isn't there yet → 0 + 1 → { yes: 1 }// 'no' → counts['no'] isn't there yet → 0 + 1 → { yes: 1, no: 1 }// 'yes' → counts['yes'] is 1 → 1 + 1 → { yes: 2, no: 1 }// 'yes' → counts['yes'] is 2 → 2 + 1 → { yes: 3, no: 1 }counts // { yes: 3, no: 1 }
You have users in an array and often need to find the one with a certain id. Instead of going through the whole array every time, you build a dictionary once, where the key is the id and the value is the whole user. Then you find anyone with a single read:
const users = [{ id: 7, name: 'Anna' }, { id: 9, name: 'Peter' }]const usersById = {}for (const u of users) {usersById[u.id] = u // store the whole user under the key id}// usersById = { 7: { id: 7, name: 'Anna' }, 9: { id: 9, name: 'Peter' } }usersById[9] // { id: 9, name: 'Peter' }usersById[9].name // 'Peter'
Read two properties of an object and make a copy of it with one extra property.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
Loading the interactive part…
Six small functions for working with a user account, a form, a basket and a price list. For each one, a comment in the code says what it's good for, shows how it's used and points to the part of the lesson that explains how.
Medium and hard challenges unlock once you sign in. Like the whole course, they are free – just an e-mail, no password and no payment.
undefined, but reading from undefined crashes – ?. helps.{ name } and computed key { [field]: value }.name: n), give a default value (= 'guest') and collect the rest (...rest).Object.keys / values / entries turn an object into an array, fromEntries turns it back.