ES2015 Destructuring & More
ES2015 has introduced a lot of new features to the JavaScript language, including arrow functions, template literals, and more, and we’re far from done in this series. Next up: Destructuring… and what else that brings, like simulating named parameters.
Note: Most of the examples in this document can be found in playground form at https://runkit.com/kerrishotts/es2015-destructuring-and-named-parameters; the examples are in the same order, so you should be able to follow along without much difficulty.
Destructuring may sound like a scary word, but it’s not, really. The feature itself is just a way of extracting data from inside a structure, which is something we do quite often — just in a fairly verbose manner.
Consider if we had an array containing date components (ordered as year, month, day), for example. How would we extract the contents of that array into variables we could work with? Using ES5, we’d do something like this:
let arr = [2017, 7, 1],
year = arr[0],
month = arr[1],
day = arr[2];
Also common is the need to extract information from an object into local variables, and we’d again use a similar pattern in ES5:
let aDate = {
year: 2017,
month: 7,
day: 1
};let year = aDate.year,
month = aDate.month,
day = aDate.day;
This is a lot of repetition, and if you’re like me, well… I want to avoid that as much as possible. Destructuring is just a way to tell JavaScript what pieces of data one needs to extract from a given structure. This new feature comes with some new syntax, and like most things, it can be abused, so if you find yourself trying to be too clever by half and scratching your head when looking at some destructuring you’ve just written, consider breaking your destructuring into steps.
So what does destructuring look like, anyway? There are two forms as you’ve probably already guessed from our ES5 examples: array destructuring and object destructuring. Destructuring an array looks like this:
let year, month, day;
[year, month, day] = [2017, 7, 1];// year = 2017, month = 7, day = 1
and destructuring an object looks like this:
let year, month, day;
({year, month, day} = {year: 2017, month: 7, day: 1});// year = 2017, month = 7, day = 1
Often you’ll find the condensed version which includes the let
(or var
or const
) keyword alongside the destructuring assignment — in this case, the variables are defined as part of the destructuring:
let [year, month, day] = [2017, 7, 1]; // arraylet {year, month, day} = {year: 2017, month: 7, day: 1}; // object
Note: Oh hey — did you notice that the shorthand object destructuring version did away with the parentheses? Did you notice the parentheses at all? Because object destructuring uses curly braces, there needs to be a way to disambiguate between a block of code and the destructuring assignment. If we use
let
,const
, or evenvar
, there’s no ambiguity. If we don’t use those, then there are multiple interpretations, and we have to use the parentheses to let the JavaScript engine know we’re trying to destructure instead of creating a new code block.
It’s worth mentioning that a destructuring assignment can be applied to object properties as well — not just variables, so the following is perfectly acceptable:
let christmasDay = {};[christmasDay.month, christmasDay.day] = [12, 25];// christmasDay = { month: 12, day: 25 }
Skipping Data
So far we’ve illustrated destructuring packets of data that have exactly the same structure on both sides. That’s not a requirement, though. It’s possible to ignore portions of the data packet, and it’s also possible to ask for more data than is actually present.
Consider the immediately previous example using Christmas Day. Let’s imagine instead that the array also included a year as the first element. We could easily skip over that element like this:
[, christmasDay.month, christmasDay.day] = [2017, 12, 25];
Notice the comma immediately after the opening bracket on the left-hand side of the assignment — that’s skipping over the first element. We can have as many commas as needed, and in any position.
What if the year had been at the end of the array instead (as is typical of dates in the USA) — well, nothing is needed, actually, since any items not specified are ignored:
[christmasDay.month, christmasDay.day] = [12, 25, 2017];
Collecting Remaining Data
There may well be times when we’ll want to siphon off some portions of an array into variables, and then collect the rest of the array into another variable (as an array). Imagine we’ve been given a string containing a list of color names. The first color in the string is our favorite, but we also want it on record that we like the other colors in the list as well.
We can use the “rest” operator (...
) to accomplish this. For now it only works when destructuring arrays, but there is a chance it’ll be able to destructure objects in the future as well (there’s a proposal out there, but it hasn’t quite reached the final stage).
So how would we extract our favorite color and collect the remaining colors? Like this:
let str = "purple blue pink red white black",
[favoriteColor, ...likedColors] = str.split(/\s+/);// favoriteColor = "purple"
// likedColors = ["blue", "pink", "red", "white", "black"]
Handling Missing Data
But what if we asked for more elements than actually existed? Ah — excellent question. It turns out that we’d just get back undefined
— just like we would if we indexed an array using an index that’s out of bounds:
let [hours, minutes, seconds] = [8, 30];// seconds = undefined
The same applies when we’re destructuring objects, although now we’re using property keys instead of indexes, of course. To ignore a property, we simply don’t use the key. If we do try to use a key that doesn’t exist, that’s OK — we’ll just get undefined
as if we had tried to use obj.nonexistentKey
. For example:
let {hours, minutes, seconds} = {hours: 8, minutes: 30};
// seconds = undefinedlet {hours, minutes} = {hours: 8, minutes: 30, seconds: 32};
// seconds is not defined
Of course, since we’re using property keys, we don’t have to specify them in the same order as the object being destructured may have them, either. {minutes, hours}
is just as valid as {hours, minutes}
as far as JavaScript is concerned. Your linter rules and agreed-upon coding style, of course, may have different opinions.
Default Values
But… what if we wanted more than undefined
when we ask for more than the structure can provide? As it turns out, you can specify default values in case data is missing, like so:
let [hours = 0, minutes = 0, seconds = 0] = [8, 30];
// seconds = 0 instead of undefinedlet {hours = 0, minutes = 0, seconds = 0} = {hours: 8, minutes: 30};
// seconds = 0 instead of undefined
The syntax makes sense in the context of arrays, but let me tell you — seeing equal signs in the middle of what looks like an object literal took me a few reads before it felt comfortable. If I’m totally honest, it probably still isn’t entirely comfortable, but it’s a work in progress.
It’s very important to mention that returning the default value occurs when undefined
is encountered, so we can actually provide an undefined
element or value and the default will still be supplied. null
on the other hand, will pass through unscathed. Case in point:
let {hours = 0, minutes = 0, seconds = 0} =
{hours: 8, minutes: null, seconds: undefined};// minutes = null; seconds = 0
Nested Destructuring
As you’ve seen thus far, destructuring is all about extracting data from structures, and oftentimes structures are more complex than a single array or a flat object. The syntax used happens to be arbitrarily nestable, as seen below with arrays:
let fruitPrices = [
["apple", 2.99], ["banana", 4.99],
["orange", 3.99], ["kiwi", 2.49],
["peach", 3.59], ["pear", 4.59]
];let [, [secondFruit, secondPrice]] = fruitPrices;// secondFruit = banana; secondPrice = 4.99
And with objects:
let response = {
status: 200,
result: {
author: "J.K. Rowling",
title: "Harry Potter and the Chamber of Secrets",
price: 11.99
}
};
let {status, result: { price }} = response;// status = 200; price = 11.99
It is important to remember that when using nested destructuring, the element or property being destructured must not be missing, undefined
or null
. In the above example, if the response
object didn’t have a result
property, the destructuring would throw a TypeError
exception. If you aren’t certain a property or index exists on an item, you could deconstruct in steps, being sure to guard against null
or undefined
:
let {status, result} = response,
{price} = result || {};
… but remember — we can provide default values if we need, so we can actually write:
let {status, result: { price } = {}} = response;
Note the = {}
— this is the default value for the result
key, so if result
is missing or undefined, an empty object will be substituted instead. Destructuring this object is fine, although price
will be set to undefined
.
Note: Guarding like this only works for missing or undefined
values. If response.result
could be null
, you’d need to destructure in steps and specifically watch for a null
value, otherwise you might still get a TypeError
exception.
Mapping Destructured Properties to Variables
So far whenever we’ve destructured an object, we’ve always been using or defining a variable of the same name. For example, in the previous example, the destructuring defined two variables status
and price
, but those also happened to be property keys on the response
object. What if we had wanted different variable names?
Well, the syntax here gets a little fuzzy, at least in my mind, so bear with me. Essentially you specify the variable name you want to use by placing it where the value of the property would be if the left-hand side were an object literal. Yeah… that didn’t make much sense, did it? Here’s an example:
//property : new variable name
let {status: httpStatusCode} = {status: 200};
console.log(httpStatusCode); // 200
Maybe that makes a little more sense? In short we’re telling JavaScript to extract the value from the status
property and use it to define httpStatusCode
. Of note is that if we’re not using let
we can bind the result to anything we want — including properties on other objects:
let someObject = {};
({status: someObject.status} = {status: 200});
// someObject.status = 200
To be honest, I don’t do this very often — I often have to take a couple passes to understand the code’s intent. But for you things may be different. Use as desired, but do be careful not to abuse it!
Destructuring Patterns
Hopefully by now, the syntax of destructuring is starting to become familiar and you can start to see how this might be useful — especially in reducing boilerplate and duplication. Now it’s time to show you some typical patterns where destructuring comes in handy.
Destructuring in loops
Destructuring can be used in conjunction with a for...of
loop, like so:
for (let [fruit, price] of fruitPrices) {
console.log(`${fruit} costs ${price}`);
}
// apple costs 2.99
// banana costs 4.99
// ...
This can come in especially handy with the ES2017 Object.entries
method which returns an array containing both the key names and values from an object, letting us write something like this:
let revenueByCompany = {
"Martha's Jams": 120000,
"Jerry's Ice": 59000,
"Clara's Pub": 79000,
"Tam's Restaurant Supplies": 405930
};
for (let [company, revenue] of Object.entries(revenueByCompany)) {
console.log(`${company} made ${revenue}`);
}// Martha's Jams made 120000
// Jerry's Ice made 59000
// ...
Note:
Object.entries
is an ES2017 feature, as such, it is not as widely supported as the ES2015 features in this article. Use with care or be sure to provide a polyfill.
Destructuring String Splits
Imagine we were given a string of the form “HH:MM:SS” — we can easily extract each component into its respective variable with the help of split
. Remember that split
will return an array containing the elements around each split point. In our case, we’ll split around the colon:
let someTime = "15:23:49",
[hours, minutes, seconds] = someTime.split(":")
.map(i => Number(i));// hours = 15, minutes = 23, seconds = 49
Destructuring Regular Expression Matches
The match
method on strings will return an array of what was captured by any capturing groups in the regular expression. Let’s say we were given a string containing a date of the form “MonthName day, year”, but that we wanted to allow for the user typing “1st” and “3rd” and things like that. We might come up with this regular expression:
let regex = /([A-Z]*)\s*(\d+)\w*\s*,?\s*(\d*)/i;
Now, if we were given a string, say, “June 21st, 1998”, and called match
on the string, we’d get an array that looks like this:
["June 21st, 1998", "June", "21", "1998"]
The first element is just telling us everything that was matched — in our case, the entire string. It’s the elements after the first element that we care about, so we can happily skip the first element when destructuring, like so:
let [, month, day, year] = str.match(regex);
Destructuring Strings and Iterables
Oh, I hadn’t mentioned this bit yet, had I? The array pattern for destructuring works on anything that is an Iterable
, which includes strings. For example:
let str = "A113",
[building, floor, ...room] = str;
room = room.join("");
console.log(`${str} is in building ${building}, on floor ${floor}, in room ${room}`);
// A113 is in building A, on floor 1, in room 13
Now, to be honest, I’ve not found much use for destructuring strings. But for destructuring iterables, well — that means you can destructure generators, maps, and sets, for example. We haven’t covered those yet, but we will!
Multiple Return Values
While we’ve always been able to return arrays and objects from functions, destructuring makes working with functions that do so a bit easier. For example, we might have a function that returns an object containing a value or an error, and it would be useful to avoid having to collect the result into a temporary variable and then peek inside to figure out what happened. Destructuring lets us do all of that in one step.
Consider a simple function called div
— it divides two numbers and returns the result using an object like {result: resultOfDivision}
. But if the divisor is zero, the result is not going to be pretty. So the function returns an error using an object like {error: "can't divide by zero"}
. The function is written this way:
function div(a, b) {
if (b === 0) {
return {
error: new Error("Can't divide by zero")
};
} else {
return {
result: a / b
};
}
}
If we wanted to work with the return result of this function using ES5, we’d first have to store the return value into a temporary variable. Then we could extract the error or the result. With ES2015, however, we can skip creating a temporary variable as follows:
let {error, result} = div(10, 0);
If an error does occur, error
will be an Error
instance and result
will be undefined
. If an error doesn’t occur, error
will be undefined
, and result
will contain the result of the division.
Defaults for Options
Let’s say you want to write a function that uses the geolocation API to determine the user’s current location. The API allows you to specify three options: maximumAge
, timeout
, and enableHighAccuracy
. None of these have to be provided to the API, but you’d like your API to provide some defaults in case the caller doesn’t provide them. In ES5, we’d be reduced to things like this:
var timeout = options.timeout || 30000;
… which doesn’t look too bad until we realize that if someone wants a timeout of zero milliseconds, then they’ll get the default 30,000 thanks to JavaScript’s handling of “falsy-ness”. So then we might write something like this:
var timeout = (options.timeout == undefined) ? 30000
: options.timeout;
… and of course, this all assumes that options
itself is defined as something other than undefined
or null
. Eugh.
With destructuring, though, things are much easier:
let { maximumAge = 30000, timeout = 30000,
enableHighAccuracy = true } = options || {};
Don’t forget that since the options are optional, we do still have to guard against destructuring undefined
or null
— hence the options || {}
bit (we don’t have to worry about zeros in this case, since we expect it to be an object).
Named Parameters and Default Parameter Values
So now you’ve seen how destructuring works, what the syntax is like, and some patterns where you might use it. That’s all been building to something pretty cool in ES2015: the ability to specify default values for parameters and something very similar to named parameters as might be found in other languages.
Default Parameter Values
Default values for parameters is probably the easiest to understand, so let’s start there first. As with the previous example of providing defaults for options, providing nice defaults in ES5 can often be a painful experience, especially if we have to worry about zero or false
as a valid value. In ES2015, however, we can skip over that frustration by using default values for parameters, like this:
function makePoint(x = 0, y = 0) {
return new Point(x, y);
}
As with destructuring, the default values will only occur with undefined
— not null
— so that’s something you’ll need to remember.
Named Parameters
Some languages let you pass arguments according to the parameter name instead of by parameter position. For example, in the previous example, the first parameter is always bound to x
and the second is always bound to y
. However, in some languages, we could write something like this:
let p1 = makePoint(x: 30, y: 15),
p2 = makePoint(y: -5, x: 12);
Notice that the order of the arguments here doesn’t matter, because we’re mapping each argument to a specific parameter name.
ES2015 doesn’t go quite this far, but it comes close with destructuring. Consider:
function makePoint({x = 0, y = 0} = {}) {
return new Point(x, y);
}let p1 = makePoint({x: 5, y: 10}),
p2 = makePoint({y: 10, x: 2});
So this should look a little familiar — it’s a bit like what we did when providing default values for options. We’ve provided defaults for x
and y
, and we are also guarding against the function being called without any arguments at all by using = {}
(which itself is a default parameter value). Although the syntax looks a little odd, you’ll find this pattern quite often, so you should definitely take some time to get used to it.
And nothing is precluding us from passing an object to makePoint
either:
let pointDef = {x: 5, y: 10},
p1 = makePoint(pointDef);
Everything that applies to destructuring objects applies here as well — so you can nest the named parameters, bind to different variable names, etc.
I do have a couple of issues when using named parameters like this, though, and how you’ll address them is going to be up to you and your team.
First, it’s easy to create parameter overload. For many, it’s a bad sign when a function takes a large number of arguments, and the same can be said when using named parameters as well. They’re so nice to use, though, that it can be easy to race past that arbitrary line in the sand and then you’ve got a function taking twenty arguments (just in the form of an object), and things start looking messy. How you deal with this is up to you and your team, but it’s something to be aware of.
Second, the self-documentation that happens by using named parameters and default values is wonderful. But using named parameters with JSDoc… not so much. Default values themselves are fine — JSDoc has had syntax for those for a long time. But how do you specify the types and descriptions for named parameters when they are technically combined into a single parameter?
As an example, how do you add JSDoc for our makePoint
function?
/**
* Returns a 2D point with x and y components
*
* @param {number} [x = 0] the distance on the x axis
* @param {number} [y = 0] the distance on the y axis
* @returns {Point} the 2D point
*/
If you try the above, many tools that try to understand your function will provide incorrect autocompletion and type checking. Those tools will assume that the function takes two parameters, when in reality, our function takes only one — an object.
Some suggest documenting the parameters as properties, but we don’t have a parent object to reference… so you end up with something like this:
/**
* Makes a point
*
* @param {Object} obj parameters
* @property {number} [obj.x = 0] distance on the x axis
* @property {number} [obj.y = 0] distance on the y axis
* @returns {Point}
*/
Which… might work, depending on the tools you use, but if you have any requirements that the variables listed in the JSDoc must also appear in the function, well… that obj
is going to cause problems for you.
If you let tools complete the JSDoc for you, this is what you’ll usually get:
/**
* @param {any} [{x = 0, y = 0}={}]
* @returns {Point}
*/
Eww. That’s… not ideal, is it? Unfortunately, I’ve yet to come across any better option at the moment. Until there is consensus on how this should be handled and until most tools agree, it’ll be up to you and your team… just be consistent.
Performance
So, how does using destructuring compare to the old ES5 methods in terms of performance? As we’ve seen before, ES2015+ hasn’t had the same time to “bake” as has ES5, and so engines have not had the same amount of time to optimize the new features.
http://incaseofstairs.com/six-speed/ has a great comparison available online, with statistics taken at the beginning of 2017. The usual caveats about microbenchmarks apply — they don’t always tell us much about code used in real life, so take all of this with a grain of salt. Furthermore, engines have progressed quite a bit since the beginning of the year, and will continue to do so.
First, for the really good news: using default values for parameters is apparently no different than using the ES5 method of providing defaults when using native ES2015 code (I’m ignoring the transpilers). Woohoo!
Second, simple destructuring is also identical in terms of performance. By simple, I mean accessing top-level properties and not using nested destructuring. This also doesn’t account for using “rest” (...
), but I wouldn’t consider doing so “simple destructuring”.
So far so good, then. The catch comes when doing something a bit more complicated, as is evidenced in the second row in the screenshot — complicated destructuring is slower — often by quite a bit — than doing it the ES5 way.
Would I let this last result stop me from writing code that uses destructuring? No, absolutely not. Would I avoid writing code using nested or more complicated destructuring? No, probably not. However, I would try to avoid extremely complicated or highly nested structures, but not because of the performance concern. No, I’d avoid them because of readability and maintainability. Again, I’d rather my code be easily read and understood than for it to be extremely terse and clever. The limits of complex destructuring are arbitrary, of course, and you and your team will have to work through what is considered “too much”.
Next Time
Well, that was quite a bit to go over, wasn’t it? If you want to learn even more about destructuring, be sure to consult the sources used for this post, since there’s more that there just wasn’t room to address. If you’re not entirely comfortable with how destructuring works or the syntax — don’t worry, you’re not alone! I would suggest playing around with destructuring until you become more comfortable with it using any of the numerous online ES2015+ playgrounds.
Next time we’re going to cover Promises (an ES2015 feature) and Async/Await (an ES2017 feature). Things are about to get really cool! See you then!
Like what I’m doing? Consider becoming a patron!
Creating articles, technical documentation, and contributing to open source software and the like takes a lot of time and effort! If you want to support my efforts would you consider becoming my patron on Patreon?