Destructuring Objects
We talked about destructuring arrays in the previous lecture, but we can also destructure objects.
Let's do that now! First, let's update our restaurant object with a new property called openingHours
.
const restaurant = {
// PREV PROPERTIES
openingHours: { thu: { open: 12, close: 19, }, fri: { open: 11, close: 22, }, sat: { open: 14, close: 20, }, },};
To destructure objects, we use curly braces. Then all we have to do is provide the variable names that exactly match the property names that we want to retrieve from the object.
Let's destructure our restaurant object and pull out the name
, categories
, and openingHours
.
// The order of elements does not matter.
const { name, categories, openingHours } = restaurant;
That's the same as with arrays, but now, we use the curly braces, and we have to specify the name of the properties. But just like with arrays, this now creates three brand new variables based on the restaurant
object.
console.log(name, categories, openingHours);
That's the fundamentals of destructuring objects. Once again, this is an extremely useful addition to the language, especially when we deal with the result of an API call, from another web application, like weather data. This data usually comes in the form of objects, and destructuring is a lifesaver as it allows us to write less code.
Now, what if we wanted the variable names to be different from the property names? Well, we can do it like this.
/**
* We still need to reference the property names otherwise
* JavaScript has no way of knowing what we actually want.
*/
const {
name: restaurantName,
categories: tags,
openingHours: hours,
} = restaurant;
console.log(restaurantName, tags, hours);
You can see that both logs are the same, but we were able to give the later new variable names. Again, that will be immensely helpful when dealing with third-party data.
Another useful feature of object destructuring is that when destructuring an object, for example, that we received from an API call, as I was explaining, it can be really useful to have default values when we are trying to read a property that does not exist on the object.
Usually we get undefined
when we try to read a property that does not exist. For example if we were trying to say restaurant.menu
, this would be undefined
because there is no property called menu
. Hence, we can set default values just like we can in arrays.
Let's say that we are trying to destructure the menu
.
/**
* The property starterMenu was renamed to starters and
* then assigned a dafault value which is an empty array.
*/
const { menu = [], starterMenu: starters = [] } = restaurant;
Now, in this case, this default value will not apply to starterMenu
since it does exist but it should apply to menu
because, as I was just saying, there is no property on the restaurant
object called menu
.
console.log(menu, starters);
Hence, above, indeed, we get the default value, which is the empty array, and next to that, the starter menu. Without the possibility to provide a default value, we would then get undefined
, as I was saying.
Once again, keep in mind that this is especially helpful when we do not have our data hard-coded as we have with the restaurant
object. The restaurant
object is just hard-coded data in our application. But in the real world, we usually get the data from somewhere else, and we might not always know how exactly the data looks like. Hence, it's useful to set defaults like this.
Next up, we need to talk about mutating variables while destructuring objects. We did that before with arrays when we were switching variables. With objects, it works a little bit differently.
Consider the below code snippet:
// We want to mutate `a` so that it becomes 23 and `b`, 7
let a = 111;
let b = 999;
const obj = { a: 23, b: 7, c: 14 };
Let's say we want to destructure the object obj
. We cannot write:
const { a, b } = obj;
Because a
and b
are already declared in the above code snippet. We can also not do:
let { a, b } = obj;
Because again, that would create new variables, and we already have them above.
Watch what happens when we do this:
{ a, b } = obj;
We have this error because JavaScript expects a code block when we start a line with a curly brace like above. And since we cannot assign anything to a code block, we get the error unexpected token.
To solve that error, the trick is to wrap everything into a parenthesis.
({ a, b } = obj);
console.log(a, b)
Now that we already know how object destructuring works, we now need to talk about nested objects, just like we did with nested arrays.
Let's consider two variables, open
and close
. These variables should contain the open and close hours for Friday. The openingHours
property is an object inside the restaurant
object, which contains the object fri
. And what we are interested in is the Friday object (fri
) which itself is inside the restaurant
object.
The openingHours
is an object that we already stored in a variable, and that's the object that we're going to destructure.
// In case you don't remember
const { name, openingHours, categories } = restaurant;
const { fri } = openingHours;
console.log(fri);
You can see that in the console, we indeed get our friday object. But remember that we actually want two variables, one called open and the other one called close.
This is how that works.
// You also need to provide the exact property name of the inner object
const {
fri: { open, close },
} = openingHours;
console.log(open, close);
And indeed, now we get our numbers, 11 and 23. We could, of course, take this even further and even assign different variable names just like we did previously.
const {
fri: { open: o, close: c },
} = openingHours;
To finish, let me actually show you a really cool practical application of this destructuring. For that, we're going to go back to our restaurant object and create another method.
Many times in JavaScript, we have functions with a lot of parameters. But then it can be hard to know the order of parameters for someone using this function. And so instead of defining the parameters manually, we can just pass an object into the function as an argument, and the function will then immediately destructure that object.
Here is what I mean.
const restaurant = {
// PROPERTIES
order: function (starterIndex, mainIndex) {
return [this.starterMenu[starterIndex], this.mainMenu[mainIndex]];
},
orderDelivery: function (obj) { console.log(obj); },};
restaurant.orderDelivery({ time: '22:00', address: '5 Av. Anatole France, 75007 Paris', mainIndex: 0, starterIndex: 1,});
Above in the console is the object we just defined because in the body of the orderDelivery
method we are simply logging it for now. So what we just did was calling the function and passing in an object of options.
That's a pretty standard thing actually in JavaScript, especially in third party libraries. Because now, in the function (orderDelivery
) arguments we can actually do destructuring right away.
const restaurant = {
// PROPERTIES
order: function (starterIndex, mainIndex) {
return [this.starterMenu[starterIndex], this.mainMenu[mainIndex]];
},
orderDelivery: function ({ time, address, mainIndex, starterIndex }) { console.log( `Order received! ${this.starterMenu[starterIndex]} and ${this.mainMenu[mainIndex]} will be delivered to ${address} at ${time}` ); },};
We now get the above complete string based on the data that we passed in the object. What's important to note here is that, we only passed in one object into the orderDelivery
function. We did not pass four arguments. It's really just one argument, one object.
Then in the orderDelivery
method, as we receive that object, we do immediately destructuring. And so that's why the names or properties we extract or destructure in the curly braces need to be exactly the names that we have in the object passed as argument.
What's great about this, is that, the properties in the object that we passed as argument, don't have to match the order in which we do destructuring in the function. And so that makes it really easy for the user of this function to specify basically the arguments.
We can even use some more knowledge that we gained here, which is to provide default values to the destructured variables.
const restaurant = {
// PROPERTIES
order: function (starterIndex, mainIndex) {
return [this.starterMenu[starterIndex], this.mainMenu[mainIndex]];
},
orderDelivery: function ({ time = '19:00',
address,
mainIndex = 1,
starterIndex = 0,
}) {
console.log(
`Order received! ${this.starterMenu[starterIndex]} and ${this.mainMenu[mainIndex]} will be delivered to ${address} at ${time}`
);
},
};
And so now as we call the method:
/**
* Only the address and starterIndex was specified. The rest will
* be taken from the default values that was set for destructuring
*/
restaurant.orderDelivery({
address: '5 Av. Anatole France, 75007 Paris',
starterIndex: 2,
});
In the console, we get ...will be delivered to 5 Av. Anatole France, 75007 Paris at 19:00. In the above object that we passed, we do not have any property for the time. Hence, as JavaScript did destructuring, it took the default value of 19. And the same happened for the mainIndex
that we also did define. The default value is 1 and 1 is Pasta. And that's why we get Pasta in the output string.
So if you ever need to write a function like this, so a really complex one with a lot of parameters that might be then hard to specify, keep this technique in mind. And this becomes even more useful as the amount of parameters increases.