Destructuring Assignment of Variables
# Destructuring Assignment of Variables
# Array Destructuring Assignment
# Basic Usage
ES6 allows extracting values from arrays and objects and assigning them to variables according to a certain pattern, which is known as destructuring (Destructuring).
Previously, assigning values to variables could only be done by specifying values directly.
let a = 1;
let b = 2;
let c = 3;
2
3
ES6 allows the following syntax.
let [a, b, c] = [1, 2, 3];
The above code shows that values can be extracted from arrays and assigned to variables according to their corresponding positions.
Essentially, this syntax is "pattern matching" -- as long as the patterns on both sides of the equals sign are the same, the variable on the left will be assigned the corresponding value. Here are some examples of destructuring with nested arrays.
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
If destructuring fails, the variable's value equals undefined.
let [foo] = [];
let [bar, foo] = [1];
2
Both cases above are failed destructurings, and foo's value will be undefined in both.
Another case is incomplete destructuring, where the pattern on the left side of the equals sign only matches part of the array on the right side. In this case, destructuring can still succeed.
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
2
3
4
5
6
7
8
Both examples above are incomplete destructurings, but they succeed.
If the right side of the equals sign is not an array (or more precisely, not an iterable structure, see the Iterator chapter), an error will be thrown.
// All throw errors
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
2
3
4
5
6
7
The above statements all throw errors because the values on the right side either don't have an Iterator interface when converted to objects (the first five expressions), or inherently lack an Iterator interface (the last expression).
For Set structures, array destructuring assignment can also be used.
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
2
In fact, as long as a data structure has an Iterator interface, array-style destructuring assignment can be used.
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
2
3
4
5
6
7
8
9
10
11
In the above code, fibs is a Generator function (see the Generator Functions chapter), which natively has an Iterator interface. The destructuring assignment will sequentially get values from this interface.
# Default Values
Destructuring assignment allows specifying default values.
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
2
3
4
5
Note that ES6 internally uses the strict equality operator (===) to determine whether a position has a value. Therefore, a default value only takes effect when an array member is strictly equal to undefined.
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
2
3
4
5
In the above code, if an array member is null, the default value does not take effect because null is not strictly equal to undefined.
If a default value is an expression, that expression is lazily evaluated -- it is only evaluated when needed.
function f() {
console.log('aaa');
}
let [x = f()] = [1];
2
3
4
5
In the above code, because x can get a value, the function f is never executed. The above code is actually equivalent to the following.
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
2
3
4
5
6
Default values can reference other variables from the destructuring assignment, but those variables must have already been declared.
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
2
3
4
The last expression above throws an error because when x uses y as its default value, y has not been declared yet.
# Object Destructuring Assignment
# Introduction
Destructuring can be used not only for arrays but also for objects.
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
2
3
There is one important difference between object and array destructuring. Array elements are ordered, and the variable's value is determined by its position; object properties have no order, so the variable must have the same name as the property to get the correct value.
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
2
3
4
5
6
In the first example above, the order of the two variables on the left side of the equals sign is different from the order of the two identically named properties on the right side, but this has no effect on the values obtained. In the second example, the variable has no corresponding identically named property, so it cannot get a value and ends up as undefined.
If destructuring fails, the variable's value equals undefined.
let {foo} = {bar: 'baz'};
foo // undefined
2
In the above code, the object on the right side of the equals sign has no foo property, so the variable foo cannot get a value and equals undefined.
Object destructuring assignment makes it very convenient to assign existing object methods to variables.
// Example 1
let { log, sin, cos } = Math;
// Example 2
const { log } = console;
log('hello') // hello
2
3
4
5
6
In example 1 above, the logarithm, sine, and cosine methods of the Math object are assigned to corresponding variables, making them much more convenient to use. Example 2 assigns console.log to the variable log.
If the variable name and property name are different, you must write it as follows.
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
2
3
4
5
6
7
This actually shows that object destructuring assignment is shorthand for the following form (see the Object Extensions chapter).
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
In other words, the internal mechanism of object destructuring assignment is to first find the property with the same name, then assign it to the corresponding variable. What actually gets assigned is the latter, not the former.
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
2
3
In the above code, foo is the matching pattern and baz is the variable. What actually gets assigned is the variable baz, not the pattern foo.
Like arrays, destructuring can also be used with nested object structures.
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
2
3
4
5
6
7
8
9
10
Note that p here is a pattern, not a variable, so it is not assigned a value. If you also want p to be assigned as a variable, you can write it as follows.
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
2
3
4
5
6
7
8
9
10
11
Here is another example.
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
2
3
4
5
6
7
8
9
10
11
12
13
The above code has three destructuring assignments, for the loc, start, and line properties respectively. Note that in the last destructuring assignment for the line property, only line is a variable; loc and start are patterns, not variables.
Here is a nested assignment example.
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
2
3
4
5
6
7
If the destructuring pattern is a nested object and the parent property of the child object does not exist, an error will be thrown.
// Error
let {foo: {bar}} = {baz: 'baz'};
2
In the above code, the foo property on the left side of the equals sign corresponds to a child object. Destructuring the bar property of this child object will throw an error. The reason is simple: foo is undefined at this point, and trying to get a child property from it throws an error.
Note that object destructuring assignment can access inherited properties.
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"
2
3
4
5
6
In the above code, the prototype of object obj1 is obj2. The foo property is not obj1's own property but is inherited from obj2. Destructuring assignment can access this property.
# Default Values
Object destructuring can also specify default values.
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
The condition for a default value to take effect is that the object's property value is strictly equal to undefined.
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
2
3
4
5
In the above code, the property x equals null. Since null is not strictly equal to undefined, it is a valid assignment, and the default value 3 does not take effect.
# Important Notes
(1) If you want to use an already declared variable for destructuring assignment, you must be very careful.
// Wrong approach
let x;
{x} = {x: 1};
// SyntaxError: syntax error
2
3
4
The above syntax throws an error because the JavaScript engine interprets {x} as a code block, causing a syntax error. The only way to solve this is to avoid placing the curly braces at the beginning of a line, preventing JavaScript from interpreting them as a code block.
// Correct approach
let x;
({x} = {x: 1});
2
3
The above code wraps the entire destructuring assignment statement in parentheses, allowing it to execute correctly. See below for the relationship between parentheses and destructuring assignment.
(2) Destructuring assignment allows the pattern on the left side of the equals sign to contain no variable names at all. This enables writing very peculiar assignment expressions.
({} = [true, false]);
({} = 'abc');
({} = []);
2
3
The above expressions are meaningless but are syntactically valid and can be executed.
(3) Since arrays are essentially special objects, object property destructuring can be performed on arrays.
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
2
3
4
The above code performs object destructuring on an array. The value at key 0 of array arr is 1, and [arr.length - 1] is key 2, with the corresponding value 3. The bracket notation here is a "computed property name" (see the Object Extensions chapter).
# String Destructuring Assignment
Strings can also be destructured. This is because in this context, the string is converted into an array-like object.
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
2
3
4
5
6
Array-like objects have a length property, so you can also destructure this property.
let {length : len} = 'hello';
len // 5
2
# Destructuring Assignment of Numbers and Booleans
When destructuring assignment is performed, if the right side of the equals sign is a number or boolean, it will first be converted to an object.
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
2
3
4
5
In the above code, the wrapper objects for both numbers and booleans have a toString property, so the variable s can get a value in both cases.
The rule for destructuring assignment is that if the value on the right side of the equals sign is not an object or array, it is first converted to an object. Since undefined and null cannot be converted to objects, destructuring assignment on them will throw an error.
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
2
# Destructuring Assignment of Function Parameters
Function parameters can also use destructuring assignment.
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
2
3
4
5
In the above code, the parameter of function add appears to be an array, but the moment the argument is passed in, the array parameter is destructured into variables x and y. For the code inside the function, the parameters they interact with are x and y.
Here is another example.
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
2
Function parameter destructuring can also use default values.
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
2
3
4
5
6
7
8
In the above code, the parameter of function move is an object. By destructuring this object, the values of variables x and y are obtained. If destructuring fails, x and y take their default values.
Note that the following syntax produces different results.
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
2
3
4
5
6
7
8
The above code specifies default values for the parameters of function move, not for the variables x and y, which is why the results differ from the previous approach.
undefined triggers the default value of function parameters.
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
2
# The Parentheses Problem
Although destructuring assignment is very convenient, parsing it is not easy. For the compiler, it is impossible to know from the start whether an expression is a pattern or an expression -- this can only be determined when (or if) the equals sign is reached.
This raises the question: what happens if parentheses appear in the pattern? The ES6 rule is that parentheses must not be used whenever they could cause ambiguity in destructuring.
However, this rule is not easy to distinguish in practice and can be quite troublesome to handle. Therefore, the recommendation is to avoid placing parentheses in patterns whenever possible.
# Cases Where Parentheses Cannot Be Used
The following three types of destructuring assignment must not use parentheses.
(1) Variable declaration statements
// All throw errors
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
2
3
4
5
6
7
8
9
All 6 statements above throw errors because they are all variable declaration statements, and patterns cannot use parentheses.
(2) Function parameters
Function parameters are also variable declarations, so they cannot contain parentheses.
// Error
function f([(z)]) { return z; }
// Error
function f([z,(x)]) { return x; }
2
3
4
(3) Patterns in assignment statements
// All throw errors
({ p: a }) = { p: 42 };
([a]) = [5];
2
3
The above code wraps the entire pattern in parentheses, causing an error.
// Error
[({ p: a }), { x: c }] = [{}, {}];
2
The above code wraps part of the pattern in parentheses, causing an error.
# Cases Where Parentheses Can Be Used
There is only one case where parentheses can be used: the non-pattern part of an assignment statement.
[(b)] = [3]; // Correct
({ p: (d) } = {}); // Correct
[(parseInt.prop)] = [3]; // Correct
2
3
All three statements above can execute correctly because, first, they are all assignment statements, not declaration statements; and second, their parentheses are not part of the pattern. In the first statement, the pattern extracts the first member of the array, unrelated to the parentheses; in the second statement, the pattern is p, not d; the third statement has the same nature as the first.
# Practical Uses
Destructuring assignment of variables has many practical uses.
(1) Swapping Variable Values
let x = 1;
let y = 2;
[x, y] = [y, x];
2
3
4
The above code swaps the values of variables x and y. This syntax is not only concise but also readable, with very clear semantics.
(2) Returning Multiple Values from a Function
A function can only return one value. To return multiple values, they must be placed in an array or object. With destructuring assignment, extracting these values becomes very convenient.
// Returning an array
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// Returning an object
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(3) Defining Function Parameters
Destructuring assignment makes it easy to map a set of parameters to variable names.
// Parameters are an ordered set of values
function f([x, y, z]) { ... }
f([1, 2, 3]);
// Parameters are an unordered set of values
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
2
3
4
5
6
7
(4) Extracting JSON Data
Destructuring assignment is especially useful for extracting data from JSON objects.
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
2
3
4
5
6
7
8
9
10
The above code can quickly extract JSON data values.
(5) Default Values for Function Parameters
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
2
3
4
5
6
7
8
9
10
11
Specifying default values for parameters avoids writing statements like var foo = config.foo || 'default foo'; inside the function body.
(6) Iterating Over Map Structures
Any object that has deployed an Iterator interface can be traversed with a for...of loop. Map structures natively support the Iterator interface, and combined with destructuring assignment, getting key names and values becomes very convenient.
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
2
3
4
5
6
7
8
9
If you only want to get the key names, or only the values, you can write it as follows.
// Getting key names
for (let [key] of map) {
// ...
}
// Getting values
for (let [,value] of map) {
// ...
}
2
3
4
5
6
7
8
9
(7) Importing Specific Methods from Modules
When loading modules, you often need to specify which methods to import. Destructuring assignment makes import statements very clear.
const { SourceMapConsumer, SourceNode } = require("source-map");