This blog post is outdated. Please read Sect. “The destructuring algorithm” in “Exploring ES6”.
This blog post looks at destructuring from a different angle: as a recursive matching algorithm. At the end, I’ll use this new knowledge to explain one especially tricky case of destructuring.
This section gives a brief overview of destructuring. For further details, consult the blog post “Destructuring and parameter handling in ECMAScript 6”.
The following code is an example of destructuring:
let obj = { first: 'Jane', last: 'Doe' };
let { first: f, last: l } = obj; // (A)
// f = 'Jane'; l = 'Doe'
In line (A) we destructure obj
: we extract data from it via a pattern on the left-hand side of the assignment operator (=
) and assign that data to the variables f
and l
. These variables are automatically declared beforehand, because the line starts with a let
.
You can destructure arrays, too:
let [x, y] = ['a', 'b']; // x = 'a'; y = 'b'
Destructuring can be used in the following locations:
// Variable declarations:
let [x] = ['a'];
const [x] = ['a'];
var [x] = ['a'];
// Assignments:
[x] = ['a'];
// Parameter definitions:
function f([x]) { ··· }
f(['a']);
The following is a destructuring assignment.
pattern = value
We want to use pattern
to extract data from value
. In the following sections, I describe an algorithm for doing so. It is known in functional programming as matching. The previous destructuring assignment is processed via
pattern ← value
That is, the operator ←
(“match against”) matches pattern
against value
. The algorithm is specified via recursive rules that take apart both operands of the ←
operator. The declarative notation may take some getting used to, but it makes the specification of the algorithm more concise. Each rule has two parts:
I only show the algorithm for destructuring assignment. Destructuring variable declarations and destructuring parameter definitions work similarly.
A pattern is either:
x
{«properties»}
[«elements»]
Each of the following sections covers one of these three cases.
(1) x ← value
(including undefined
and null
)
x = value
(2a) {«properties»} ← undefined
throw new TypeError();
(2b) {«properties»} ← null
throw new TypeError();
(2c) {key: pattern, «properties»} ← obj
pattern ← obj.key
{«properties»} ← obj
(2d) {key: pattern = default_value, «properties»} ← obj
let tmp = obj.key;
if (tmp !== undefined) {
pattern ← tmp
} else {
pattern ← default_value
}
{«properties»} ← obj
(2e) {} ← obj
(done)
The sub-algorithm in this section starts with an array pattern and an iterable and continues with the elements of the pattern and an iterator (obtained from the iterable). The helper functions isIterable()
and getNext()
are defined at the end of this section.
(3a) [«elements»] ← non_iterable
assert(!isIterable(non_iterable))
throw new TypeError();
(3b) [«elements»] ← iterable
assert(isIterable(iterable))
let iterator = iterable[Symbol.iterator]();
«elements» ← iterator
(3c) pattern, «elements» ← iterator
pattern ← getNext(iterator)
«elements» ← iterator
(3d) pattern = default_value, «elements» ← iterator
let tmp = getNext(iterator);
if (tmp !== undefined) {
pattern ← tmp
} else {
pattern ← default_value
}
«elements» ← iterator
(3e) , «elements» ← iterator
(hole, elision)
getNext(iterator); // skip
«elements» ← iterator
(3f) ...pattern ← iterator
(always last part!)
let tmp = [];
for (let elem of iterator) {
tmp.push(elem);
}
pattern ← tmp
(3g) ← iterator
(no elements left, nothing to do)
The rules in this section use the following two helper functions:
function getNext(iterator) {
let n = iterator.next();
if (n.done) {
return undefined;
} else {
return n.value;
}
}
function isIterable(value) {
return (value !== null
&& typeof value === 'object'
&& typeof value[Symbol.iterator] === 'function');
}
The following function definition is used to make sure that both of the named parameters x
and y
have default values and can be omitted. Additionally, = {}
enables us to omit the object literal, too (see last function call below).
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]
But why would you define the parameters as in the previous code snippet? Why not as follows – which is also completely legal ECMAScript 6?
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
To see why solution 1 is correct, let’s use both solutions for two examples.
For function calls, formal parameters (inside function definitions) are matched against actual parameters (inside function calls). As an example, take the following function definition and the following function call.
function func(a=0, b=0) { ··· }
func(1, 2);
The parameters a
and b
are set up similarly to the following destructuring.
[a=0, b=0] ← [1, 2]
Let’s examine how destructuring works for move()
.
Example 1. move()
leads to this destructuring:
[{x, y} = { x: 0, y: 0 }] ← []
The only array element on the left-hand side does not have a match on the right-hand side, which is why the default value is used (rules 3b, 3d):
{x, y} ← { x: 0, y: 0 }
The left-hand side contains property value shorthands, it is an abbreviation for:
{x: x, y: y} ← { x: 0, y: 0 }
This destructuring leads to the following two assignments (rule 2c, 1):
x = 0;
y = 0;
However, this is the only case in which the default value is used.
Example 2. Let’s examine the function call move({z:3})
which leads to the following destructuring:
[{x, y} = { x: 0, y: 0 }] ← [{z:3}]
There is an array element at index 0 on the right-hand side. Therefore, the default value is ignored and the next step is (rule 3d):
{x, y} ← { z: 3 }
That leads to both x
and y
being set to undefined
, which is not what we want.
Let’s try solution 1.
Example 1: move()
[{x=0, y=0} = {}] ← []
We don’t have an array element at index 0 on the right-hand side and use the default value (rule 3d):
{x=0, y=0} ← {}
The left-hand side contains property value shorthands, which means that this destructuring is equivalent to:
{x: x=0, y: y=0} ← {}
Neither property x
nor property y
have a match on the right-hand side. Therefore, the default values are used and the following destructurings are performed next (rule 2d):
x ← 0
y ← 0
That leads to the following assignments (rule 1):
x = 0
y = 0
Example 2: move({z:3})
[{x=0, y=0} = {}] ← [{z:3}]
The first element of the array pattern has a match on the right-hand side and that match is used to continue destructuring (rule 3d):
{x=0, y=0} ← {z:3}
Like in example 1, there are no properties x
and y
on the right-hand side and the default values are used:
x = 0
y = 0
The examples demonstrate that default values are a feature of pattern parts (object properties or array elements). If a part has no match or is matched against undefined
then the default value is used. That is, the pattern is matched against the default value, instead.