Destructuring
ES6 introduces a new syntactic feature called destructuring, which may be a little less confusing if you instead think of it as structured assignment. To understand this meaning, consider:
function foo() {
return [1,2,3];
}
var tmp = foo(),
a = tmp[0], b = tmp[1], c = tmp[2];
console.log( a, b, c ); // 1 2 3
As you can see, we created a manual assignment of the values in the array that foo()
returns to individual variables a
, b
, and c
, and to do so we (unfortunately) needed the tmp
variable.
Similarly, we can do the following with objects:
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}
var tmp = bar(),
x = tmp.x, y = tmp.y, z = tmp.z;
console.log( x, y, z ); // 4 5 6
The tmp.x
property value is assigned to the x
variable, and likewise for tmp.y
to y
and tmp.z
to z
.
Manually assigning indexed values from an array or properties from an object can be thought of as structured assignment. ES6 adds a dedicated syntax for destructuring, specifically array destructuring and object destructuring. This syntax eliminates the need for the tmp
variable in the previous snippets, making them much cleaner. Consider:
var [ a, b, c ] = foo();
var { x: x, y: y, z: z } = bar();
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
You’re likely more accustomed to seeing syntax like [a,b,c]
on the righthand side of an =
assignment, as the value being assigned.
Destructuring symmetrically flips that pattern, so that [a,b,c]
on the lefthand side of the =
assignment is treated as a kind of “pattern” for decomposing the righthand side array value into separate variable assignments.
Similarly, { x: x, y: y, z: z }
specifies a “pattern” to decompose the object value from bar()
into separate variable assignments.
Object Property Assignment Pattern
Let’s dig into that { x: x, .. }
syntax from the previous snippet. If the property name being matched is the same as the variable you want to declare, you can actually shorten the syntax:
var { x, y, z } = bar();
console.log( x, y, z ); // 4 5 6
Pretty cool, right?
But is { x, .. }
leaving off the x:
part or leaving off the : x
part? We’re actually leaving off the x:
part when we use the shorter syntax. That may not seem like an important detail, but you’ll understand its importance in just a moment.
If you can write the shorter form, why would you ever write out the longer form? Because that longer form actually allows you to assign a property to a different variable name, which can sometimes be quite useful:
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
There’s a subtle but super-important quirk to understand about this variation of the object destructuring form. To illustrate why it can be a gotcha you need to be careful of, let’s consider the “pattern” of how normal object literals are specified:
var X = 10, Y = 20;
var o = { a: X, b: Y };
console.log( o.a, o.b ); // 10 20
In { a: X, b: Y }
, we know that a
is the object property, and X
is the source value that gets assigned to it. In other words, the syntactic pattern is target: source
, or more obviously, property-alias: value
. We intuitively understand this because it’s the same as =
assignment, where the pattern is target = source
.
However, when you use object destructuring assignment — that is, putting the { .. }
object literal-looking syntax on the lefthand side of the =
operator — you invert that target: source
pattern.
Recall:
var { x: bam, y: baz, z: bap } = bar();
The syntactic pattern here is source: target
(or value: variable-alias
). x: bam
means the x
property is the source value and bam
is the target variable to assign to. In other words, object literals are target <-- source
, and object destructuring assignments are source --> target
. See how that’s flipped?
There’s another way to think about this syntax though, which may help ease the confusion. Consider:
var aa = 10, bb = 20;
var o = { x: aa, y: bb };
var { x: AA, y: BB } = o;
console.log( AA, BB ); // 10 20
In the { x: aa, y: bb }
line, the x
and y
represent the object properties. In the { x: AA, y: BB }
line, the x
and the y
also represent the object properties.
Recall how earlier I asserted that { x, .. }
was leaving off the x:
part? In those two lines, if you erase the x:
and y:
parts in that snippet, you’re left only with aa, bb
and AA, BB
, which in effect — only conceptually, not actually — are assignments from aa
to AA
and from bb
to BB
.
So, that symmetry may help to explain why the syntactic pattern was intentionally flipped for this ES6 feature.
Note: I would have preferred the syntax to be { AA: x , BB: y }
for the destructuring assignment, as that would have preserved consistency of the more familiar target: source
pattern for both usages. Alas, I’m having to train my brain for the inversion, as some readers may also have to do.
Not Just Declarations
So far, we’ve used destructuring assignment with var
declarations (of course, they could also use let
and const
), but destructuring is a general assignment operation, not just a declaration.
Consider:
var a, b, c, x, y, z;
[a,b,c] = foo();
( { x, y, z } = bar() );
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6
The variables can already be declared, and then the destructuring only does assignments, exactly as we’ve already seen.
Note: For the object destructuring form specifically, when leaving off a var
/let
/const
declarator, we had to surround the whole assignment expression in ( )
, because otherwise the { .. }
on the lefthand side as the first element in the statement is taken to be a block statement instead of an object.
In fact, the assignment expressions (a
, y
, etc.) don’t actually need to be just variable identifiers. Anything that’s a valid assignment expression is allowed. For example:
var o = {};
[o.a, o.b, o.c] = foo();
( { x: o.x, y: o.y, z: o.z } = bar() );
console.log( o.a, o.b, o.c ); // 1 2 3
console.log( o.x, o.y, o.z ); // 4 5 6
You can even use computed property expressions in the destructuring. Consider:
var which = "x",
o = {};
( { [which]: o[which] } = bar() );
console.log( o.x ); // 4
The [which]:
part is the computed property, which results in x
— the property to destructure from the object in question as the source of the assignment. The o[which]
part is just a normal object key reference, which equates to o.x
as the target of the assignment.
You can use the general assignments to create object mappings/transformations, such as:
var o1 = { a: 1, b: 2, c: 3 },
o2 = {};
( { a: o2.x, b: o2.y, c: o2.z } = o1 );
console.log( o2.x, o2.y, o2.z ); // 1 2 3
Or you can map an object to an array, such as:
var o1 = { a: 1, b: 2, c: 3 },
a2 = [];
( { a: a2[0], b: a2[1], c: a2[2] } = o1 );
console.log( a2 ); // [1,2,3]
Or the other way around:
var a1 = [ 1, 2, 3 ],
o2 = {};
[ o2.a, o2.b, o2.c ] = a1;
console.log( o2.a, o2.b, o2.c ); // 1 2 3
Or you could reorder one array to another:
var a1 = [ 1, 2, 3 ],
a2 = [];
[ a2[2], a2[0], a2[1] ] = a1;
console.log( a2 ); // [2,3,1]
You can even solve the traditional “swap two variables” task without a temporary variable:
var x = 10, y = 20;
[ y, x ] = [ x, y ];
console.log( x, y ); // 20 10
Warning: Be careful: you shouldn’t mix in declaration with assignment unless you want all of the assignment expressions also to be treated as declarations. Otherwise, you’ll get syntax errors. That’s why in the earlier example I had to do var a2 = []
separately from the [ a2[0], .. ] = ..
destructuring assignment. It wouldn’t make any sense to try var [ a2[0], .. ] = ..
, because a2[0]
isn’t a valid declaration identifier; it also obviously couldn’t implicitly create a var a2 = []
declaration to use.
Repeated Assignments
The object destructuring form allows a source property (holding any value type) to be listed multiple times. For example:
var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1
That also means you can both destructure a sub-object/array property and also capture the sub-object/array’s value itself. Consider:
var { a: { x: X, x: Y }, a } = { a: { x: 1 } };
X; // 1
Y; // 1
a; // { x: 1 }
( { a: X, a: Y, a: [ Z ] } = { a: [ 1 ] } );
X.push( 2 );
Y[0] = 10;
X; // [10,2]
Y; // [10,2]
Z; // 1
A word of caution about destructuring: it may be tempting to list destructuring assignments all on a single line as has been done thus far in our discussion. However, it’s a much better idea to spread destructuring assignment patterns over multiple lines, using proper indentation — much like you would in JSON or with an object literal value — for readability sake.
// harder to read:
var { a: { b: [ c, d ], e: { f } }, g } = obj;
// better:
var {
a: {
b: [ c, d ],
e: { f }
},
g
} = obj;
Remember: the purpose of destructuring is not just less typing, but more declarative readability.
Destructuring Assignment Expressions
The assignment expression with object or array destructuring has as its completion value the full righthand object/array value. Consider:
var o = { a:1, b:2, c:3 },
a, b, c, p;
p = { a, b, c } = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
In the previous snippet, p
was assigned the o
object reference, not one of the a
, b
, or c
values. The same is true of array destructuring:
var o = [1,2,3],
a, b, c, p;
p = [ a, b, c ] = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
By carrying the object/array value through as the completion, you can chain destructuring assignment expressions together:
var o = { a:1, b:2, c:3 },
p = [4,5,6],
a, b, c, x, y, z;
( {a} = {b,c} = o );
[x,y] = [z] = p;
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 4
Too Many, Too Few, Just Enough
With both array destructuring assignment and object destructuring assignment, you do not have to assign all the values that are present. For example:
var [,b] = foo();
var { x, z } = bar();
console.log( b, x, z ); // 2 4 6
The 1
and 3
values that came back from foo()
are discarded, as is the 5
value from bar()
.
Similarly, if you try to assign more values than are present in the value you’re destructuring/decomposing, you get graceful fallback to undefined
, as you’d expect:
var [,,c,d] = foo();
var { w, z } = bar();
console.log( c, z ); // 3 6
console.log( d, w ); // undefined undefined
This behavior follows symmetrically from the earlier stated “undefined
is missing” principle.
We examined the ...
operator earlier in this chapter, and saw that it can sometimes be used to spread an array value out into its separate values, and sometimes it can be used to do the opposite: to gather a set of values together into an array.
In addition to the gather/rest usage in function declarations, ...
can perform the same behavior in destructuring assignments. To illustrate, let’s recall a snippet from earlier in this chapter:
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
Here we see that ...a
is spreading a
out, because it appears in the array [ .. ]
value position. If ...a
appears in an array destructuring position, it performs the gather behavior:
var a = [2,3,4];
var [ b, ...c ] = a;
console.log( b, c ); // 2 [3,4]
The var [ .. ] = a
destructuring assignment spreads a
out to be assigned to the pattern described inside the [ .. ]
. The first part names b
for the first value in a
(2
). But then ...c
gathers the rest of the values (3
and 4
) into an array and calls it c
.
Note: We’ve seen how ...
works with arrays, but what about with objects? It’s not an ES6 feature, but see Chapter 8 for discussion of a possible “beyond ES6” feature where ...
works with spreading or gathering objects.
Default Value Assignment
Both forms of destructuring can offer a default value option for an assignment, using the =
syntax similar to the default function argument values discussed earlier.
Consider:
var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();
console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, w ); // 4 5 6 20
You can combine the default value assignment with the alternative assignment expression syntax covered earlier. For example:
var { x, y, z, w: WW = 20 } = bar();
console.log( x, y, z, WW ); // 4 5 6 20
Be careful about confusing yourself (or other developers who read your code) if you use an object or array as the default value in a destructuring. You can create some really hard to understand code:
var x = 200, y = 300, z = 100;
var o1 = { x: { y: 42 }, z: { y: z } };
( { y: x = { y: y } } = o1 );
( { z: y = { y: z } } = o1 );
( { x: z = { y: x } } = o1 );
Can you tell from that snippet what values x
, y
, and z
have at the end? Takes a moment of pondering, I would imagine. I’ll end the suspense:
console.log( x.y, y.y, z.y ); // 300 100 42
The takeaway here: destructuring is great and can be very useful, but it’s also a sharp sword that can cause injury (to someone’s brain) if used unwisely.
Nested Destructuring
If the values you’re destructuring have nested objects or arrays, you can destructure those nested values as well:
var a1 = [ 1, [2, 3, 4], 5 ];
var o1 = { x: { y: { z: 6 } } };
var [ a, [ b, c, d ], e ] = a1;
var { x: { y: { z: w } } } = o1;
console.log( a, b, c, d, e ); // 1 2 3 4 5
console.log( w ); // 6
Nested destructuring can be a simple way to flatten out object namespaces. For example:
var App = {
model: {
User: function(){ .. }
}
};
// instead of:
// var User = App.model.User;
var { model: { User } } = App;
Destructuring Parameters
In the following snippet, can you spot the assignment?
function foo(x) {
console.log( x );
}
foo( 42 );
The assignment is kinda hidden: 42
(the argument) is assigned to x
(the parameter) when foo(42)
is executed. If parameter/argument pairing is an assignment, then it stands to reason that it’s an assignment that could be destructured, right? Of course!
Consider array destructuring for parameters:
function foo( [ x, y ] ) {
console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined
Object destructuring for parameters works, too:
function foo( { x, y } ) {
console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined
This technique is an approximation of named arguments (a long requested feature for JS!), in that the properties on the object map to the destructured parameters of the same names. That also means that we get optional parameters (in any position) for free, as you can see leaving off the x
“parameter” worked as we’d expect.
Of course, all the previously discussed variations of destructuring are available to us with parameter destructuring, including nested destructuring, default values, and more. Destructuring also mixes fine with other ES6 function parameter capabilities, like default parameter values and rest/gather parameters.
Consider these quick illustrations (certainly not exhaustive of the possible variations):
function f1([ x=2, y=3, z ]) { .. }
function f2([ x, y, ...z], w) { .. }
function f3([ x, y, ...z], ...w) { .. }
function f4({ x: X, y }) { .. }
function f5({ x: X = 10, y = 20 }) { .. }
function f6({ x = 10 } = {}, { y } = { y: 10 }) { .. }
Let’s take one example from this snippet and examine it, for illustration purposes:
function f3([ x, y, ...z], ...w) {
console.log( x, y, z, w );
}
f3( [] ); // undefined undefined [] []
f3( [1,2,3,4], 5, 6 ); // 1 2 [3,4] [5,6]
There are two ...
operators in use here, and they’re both gathering values in arrays (z
and w
), though ...z
gathers from the rest of the values left over in the first array argument, while ...w
gathers from the rest of the main arguments left over after the first.
Destructuring Defaults + Parameter Defaults
There’s one subtle point you should be particularly careful to notice — the difference in behavior between a destructuring default value and a function parameter default value. For example:
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
At first, it would seem that we’ve declared a default value of 10
for both the x
and y
parameters, but in two different ways. However, these two different approaches will behave differently in certain cases, and the difference is awfully subtle.
Consider:
f6( {}, {} ); // 10 undefined
Wait, why did that happen? It’s pretty clear that named parameter x
is defaulting to 10
if not passed as a property of that same name in the first argument’s object.
But what about y
being undefined
? The { y: 10 }
value is an object as a function parameter default value, not a destructuring default value. As such, it only applies if the second argument is not passed at all, or is passed as undefined
.
In the previous snippet, we are passing a second argument ({}
), so the default { y: 10 }
value is not used, and the { y }
destructuring occurs against the passed in {}
empty object value.
Now, compare { y } = { y: 10 }
to { x = 10 } = {}
.
For the x
‘s form usage, if the first function argument is omitted or undefined
, the {}
empty object default applies. Then, whatever value is in the first argument position — either the default {}
or whatever you passed in — is destructured with the { x = 10 }
, which checks to see if an x
property is found, and if not found (or undefined
), the 10
default value is applied to the x
named parameter.
Deep breath. Read back over those last few paragraphs a couple of times. Let’s review via code:
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
f6( undefined, undefined ); // 10 10
f6( {}, undefined ); // 10 10
f6( {}, {} ); // 10 undefined
f6( undefined, {} ); // 10 undefined
f6( { x: 2 }, { y: 3 } ); // 2 3
It would generally seem that the defaulting behavior of the x
parameter is probably the more desirable and sensible case compared to that of y
. As such, it’s important to understand why and how { x = 10 } = {}
form is different from { y } = { y: 10 }
form.
If that’s still a bit fuzzy, go back and read it again, and play with this yourself. Your future self will thank you for taking the time to get this very subtle gotcha nuance detail straight.
Nested Defaults: Destructured and Restructured
Although it may at first be difficult to grasp, an interesting idiom emerges for setting defaults for a nested object’s properties: using object destructuring along with what I’d call restructuring.
Consider a set of defaults in a nested object structure, like the following:
// taken from: http://es-discourse.com/t/partial-default-arguments/120/7
var defaults = {
options: {
remove: true,
enable: false,
instance: {}
},
log: {
warn: true,
error: true
}
};
Now, let’s say that you have an object called config
, which has some of these applied, but perhaps not all, and you’d like to set all the defaults into this object in the missing spots, but not override specific settings already present:
var config = {
options: {
remove: false,
instance: null
}
};
You can of course do so manually, as you might have done in the past:
config.options = config.options || {};
config.options.remove = (config.options.remove !== undefined) ?
config.options.remove : defaults.options.remove;
config.options.enable = (config.options.enable !== undefined) ?
config.options.enable : defaults.options.enable;
...
Yuck.
Others may prefer the assign-overwrite approach to this task. You might be tempted by the ES6 Object.assign(..)
utility (see Chapter 6) to clone the properties first from defaults
and then overwritten with the cloned properties from config
, as so:
config = Object.assign( {}, defaults, config );
That looks way nicer, huh? But there’s a major problem! Object.assign(..)
is shallow, which means when it copies defaults.options
, it just copies that object reference, not deep cloning that object’s properties to a config.options
object. Object.assign(..)
would need to be applied (sort of “recursively”) at all levels of your object’s tree to get the deep cloning you’re expecting.
Note: Many JS utility libraries/frameworks provide their own option for deep cloning of an object, but those approaches and their gotchas are beyond our scope to discuss here.
So let’s examine if ES6 object destructuring with defaults can help at all:
config.options = config.options || {};
config.log = config.log || {};
({
options: {
remove: config.options.remove = defaults.options.remove,
enable: config.options.enable = defaults.options.enable,
instance: config.options.instance = defaults.options.instance
} = {},
log: {
warn: config.log.warn = defaults.log.warn,
error: config.log.error = defaults.log.error
} = {}
} = config);
Not as nice as the false promise of Object.assign(..)
(being that it’s shallow only), but it’s better than the manual approach by a fair bit, I think. It is still unfortunately verbose and repetitive, though.
The previous snippet’s approach works because I’m hacking the destructuring and defaults mechanism to do the property === undefined
checks and assignment decisions for me. It’s a trick in that I’m destructuring config
(see the = config
at the end of the snippet), but I’m reassigning all the destructured values right back into config
, with the config.options.enable
assignment references.
Still too much, though. Let’s see if we can make anything better.
The following trick works best if you know that all the various properties you’re destructuring are uniquely named. You can still do it even if that’s not the case, but it’s not as nice — you’ll have to do the destructuring in stages, or create unique local variables as temporary aliases.
If we fully destructure all the properties into top-level variables, we can then immediately restructure to reconstitute the original nested object structure.
But all those temporary variables hanging around would pollute scope. So, let’s use block scoping (see “Block-Scoped Declarations” earlier in this chapter) with a general { }
enclosing block:
// merge `defaults` into `config`
{
// destructure (with default value assignments)
let {
options: {
remove = defaults.options.remove,
enable = defaults.options.enable,
instance = defaults.options.instance
} = {},
log: {
warn = defaults.log.warn,
error = defaults.log.error
} = {}
} = config;
// restructure
config = {
options: { remove, enable, instance },
log: { warn, error }
};
}
That seems a fair bit nicer, huh?
Note: You could also accomplish the scope enclosure with an arrow IIFE instead of the general { }
block and let
declarations. Your destructuring assignments/defaults would be in the parameter list and your restructuring would be the return
statement in the function body.
The { warn, error }
syntax in the restructuring part may look new to you; that’s called “concise properties” and we cover it in the next section!