Destructuring Assignment in ECMAScript 6
What is destructuring assignment?
Destructuring assignment allows you to assign the properties of an array or object to variables using syntax that looks similar to array or object literals. This syntax can be extremely terse, while still exhibiting more clarity than the traditional property access.
Without destructuring assignment, you might access the first three items in an array like this:
let first = someArray[0];
let second = someArray[1];
let third = someArray[2];
With destructuring assignment, the equivalent code becomes more concise and read-able:
let [first, second, third] = someArray;
TC39 (the governing committee of ECMAScript) has already reached consensus on destructuring assignment and it is part of the draft ES6 specification. Effectively what this means is that it is now up to the people writing JavaScript engines to start implementing it; SpiderMonkey (Firefox's JS engine) already has support for much of it. Track SpiderMonkey's destructuring (and general ES6) support in this bugzilla ticket.
Destructuring Arrays
We already saw one example of destructuring assignment on an array above. The general form of the syntax is:
[ variable1, variable2, ..., variableN ] = array;
This will just assign variable1 through variableN to the corresponding item in
the array. If you want to declare your variables at the same time, you can add a
var
, let
, or const
in front of the assignment:
var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;
Although variable
is a misnomer since you can nest patterns as deep as you
would like:
var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3
Furthermore, you can skip over items in the array being destructured:
var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"
And you can capture all trailing items in an array with a "rest" pattern:
var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]
When you access items in the array that are out of bounds or don't exist, you
get the same result you would by indexing: undefined
.
console.log([][0]);
// undefined
var [missing] = [];
console.log(missing);
// undefined
Destructuring Objects
Destructuring on objects lets you bind variables to different properties of an object. You specify the property being bound, followed by the variable you are binding its value to.
var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };
var { name: nameA } = robotA;
var { name: nameB } = robotB;
console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"
There is a helpful syntactical shortcut for when the property and variable names are the same:
var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"
And just like destructuring on arrays, you can nest and combine further destructuring:
var complicatedObj = {
arrayProp: [
"Zapp",
{ second: "Brannigan" }
]
};
var { arrayProp: [first, { second }] } = complicatedObj;
console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"
When you destructure on properties that are not defined, you get undefined
:
var { missing } = {};
console.log(missing);
// undefined
One potential gotcha you should be aware of is when you are using destructuring
on an object to assign variables, but not to declare them (when there is no
let
, const
, or var
):
{ blowUp } = { blowUp: 10 };
// Syntax error
This happens because the engine attempts to parse the expression as a block
statement (for example, { console }
is a valid block statement). The solution
is to either wrap the pattern or the whole expression in parenthesis:
({ safe }) = {};
({ andSound } = {});
// No errors
Destructuring Values that are not an Object or Array
When you try to use destructuring on null
or undefined
, you get a type
error:
var [blowUp] = null;
// TypeError: null has no properties
However, you can destructure on other primitive types such as booleans, numbers,
and strings, and get undefined
:
var [wtf] = NaN;
console.log(wtf);
// undefined
This may come unexpected, but upon further examination it turns out to be
simple. When a value is being destructured, it is first
converted to an object using the abstract operation ToObject. Most
types can be converted to an object, but null
and undefined
may not be.
Default Values
You can also provide default values for when the property you are destructuring is not defined:
var [missing = true] = [];
console.log(missing);
// true
var { x = 3 } = {};
console.log(x);
// 3
Practical Applications of Destructuring
Function Parameter Definitions
As developers, we can often expose nicer APIs by accepting a single object with multiple properties as a parameter instead of forcing our API consumers to remember the order of many individual parameters. We can use destructuring to avoid repeating this single parameter object whenever we want to reference one of its properties:
function removeBreakpoint({ url, line, column }) {
// ...
}
This is a real world snippet of code from Firefox's debugger. We use this all over the place, it is crazy nice.
Configuration Object Parameters
Expanding on the previous example, we can also give default values to the
properties of the objects we are destructuring. This is particularly helpful
when we have an object that is meant to provide configuration and many of the
object's properties already have sensible defaults. For example, jQuery's ajax
function takes a configuration object as its second parameter, and could be
re-written like this:
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};
This avoids repeating var foo = config.foo || 'default foo';
for each property of
the configuration object.
With the ES6 Iteration Protocol
ECMAScript 6 also defines an iteration protocol somewhat similar to
the one in Python. When you iterate over
Map
s (an ES6 addition to the standard library), you get a series of
[key, value]
pairs. We can destructure this pair to get easy access to both
the key and the value:
var map = new Map();
map.set(window, "the global");
map.set(document, "the document");
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// "[object Window] is the global"
// "[object HTMLDocument] is the document"
Iterate over only the keys:
for (let [key] of map) {
// ...
}
Or iterate over only the values:
for (let [,value] of map) {
// ...
}
Multiple Return Values
I've written about this once before. Returning multiple values from a function is a lot more practical when you can destructure the result:
function returnMultipleValues() {
return [1, 2];
}
var [foo, bar] = returnMultipleValues();
Alternatively, you can use an object as the container and name the returned values:
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = returnMultipleValues();
These both end up much better than holding onto the temporary container:
function returnMultipleValues() {
return {
foo: 1,
bar: 2
};
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;
Importing a Subset of a Module
When importing some module X into your own module Y, it is fairly common that module X exports more functions than you are using. With destructuring, you can be explicit about which parts of module X are being used in module Y:
const { SourceMapConsumer, SourceNode } = require("source-map");