Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

3 The destructuring algorithm



In this chapter, we look at destructuring from a different angle: as a recursive pattern matching algorithm.

The algorithm will give us a better understanding of default values. That will be useful at the end, where we’ll try to figure out how the following two functions differ:

function move({x=0, y=0} = {})         { ··· }
function move({x, y} = { x: 0, y: 0 }) { ··· }

3.1 Preparing for the pattern matching algorithm

A destructuring assignment looks like this:

«pattern» = «value»

We want to use pattern to extract data from value.

We will now look at an algorithm for performing this kind of assignment. This algorithm is known in functional programming as pattern matching (short: matching). It specifies the operator (“match against”) that matches a pattern against a value and assigns to variables while doing so:

«pattern» ← «value»

We will only explore destructuring assignment, but destructuring variable declarations and destructuring parameter definitions work similarly. We won’t go into advanced features, either: Computed property keys, property value shorthands, and object properties and array elements as assignment targets, are beyond the scope of this chapter.

The specification for the match operator consists of declarative rules that descend into the structures of both operands. The declarative notation may take some getting used to, but it makes the specification more concise.

3.1.1 Using declarative rules for specifying the matching algorithm

The declarative rules used in this chapter operate on input and produce the result of the algorithm via side effects. This is one such rule (which we’ll see again later):

This rule has the following parts:

In rule (2c), the head means that this rule can be applied if there is an object pattern with at least one property (whose key is key) and zero or more remaining properties. The effect of this rule is that execution continues with the property value pattern being matched against obj.key and the remaining properties being matched against obj.

Let’s consider one more rule from this chapter:

In rule (2e), the head means that this rule is executed if the empty object pattern {} is matched against a value obj. The body means that, in this case, we are done.

Together, rule (2c) and rule (2e) form a declarative loop that iterates over the properties of the pattern on the left-hand side of the arrow.

3.1.2 Evaluating expressions based on the declarative rules

The complete algorithm is specified via a sequence of declarative rules. Let’s assume we want to evaluate the following matching expression:

{first: f, last: l} ← obj

To apply a sequence of rules, we go over them from top to bottom and execute the first applicable rule. If there is a matching expression in the body of that rule, the rules are applied again. And so on.

Sometimes the head includes a condition that also determines if a rule is applicable – for example:

3.2 The pattern matching algorithm

3.2.1 Patterns

A pattern is either:

The next three sections specify rules for handling these three cases in matching expressions.

3.2.2 Rules for variable

3.2.3 Rules for object patterns

Rules 2a and 2b deal with illegal values. Rules 2c–2e loop over the properties of the pattern. In rule 2d, we can see that a default value provides an alternative to match against if there is no matching property in obj.

3.2.4 Rules for Array patterns

Array pattern and iterable. The algorithm for Array destructuring starts with an Array pattern and an iterable:

Helper function:

function isIterable(value) {
  return (value !== null
    && typeof value === 'object'
    && typeof value[Symbol.iterator] === 'function');
}

Array elements and iterator. The algorithm continues with:

These are the rules:

Helper function:

function getNext(iterator) {
  const {done,value} = iterator.next();
  return (done ? undefined : value);
}

An iterator being finished is similar to missing properties in objects.

3.3 Empty object patterns and Array patterns

Interesting consequence of the algorithm’s rules: We can destructure with empty object patterns and empty Array patterns.

Given an empty object pattern {}: If the value to be destructured is neither undefined nor null, then nothing happens. Otherwise, a TypeError is thrown.

const {} = 123; // OK, neither undefined nor null
assert.throws(
  () => {
    const {} = null;
  },
  /^TypeError: Cannot destructure 'null' as it is null.$/)

Given an empty Array pattern []: If the value to be destructured is iterable, then nothing happens. Otherwise, a TypeError is thrown.

const [] = 'abc'; // OK, iterable
assert.throws(
  () => {
    const [] = 123; // not iterable
  },
  /^TypeError: 123 is not iterable$/)

In other words: Empty destructuring patterns force values to have certain characteristics, but have no other effects.

3.4 Applying the algorithm

In JavaScript, named parameters are simulated via objects: The caller uses an object literal and the callee uses destructuring. This simulation is explained in detail in “JavaScript for impatient programmers”. The following code shows an example: function move1() has two named parameters, x and y:

function move1({x=0, y=0} = {}) { // (A)
  return [x, y];
}
assert.deepEqual(
  move1({x: 3, y: 8}), [3, 8]);
assert.deepEqual(
  move1({x: 3}), [3, 0]);
assert.deepEqual(
  move1({}), [0, 0]);
assert.deepEqual(
  move1(), [0, 0]);

There are three default values in line A:

But why would we define the parameters as in the previous code snippet? Why not as follows?

function move2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

To see why move1() is correct, we are going to use both functions in two examples. Before we do that, let’s see how the passing of parameters can be explained via matching.

3.4.1 Background: passing parameters via matching

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]

3.4.2 Using move2()

Let’s examine how destructuring works for move2().

Example 1. The function call move2() leads to this destructuring:

[{x, y} = { x: 0, y: 0 }] ← []

The single Array element on the left-hand side does not have a match on the right-hand side, which is why {x,y} is matched against the default value and not against data from the right-hand side (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 (rules 2c, 1):

x = 0;
y = 0;

This is what we wanted. However, in the next example, we are not as lucky.

Example 2. Let’s examine the function call move2({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. The problem is that {x,y} is not matched against the default value, anymore, but against {z:3}.

3.4.3 Using move1()

Let’s try move1().

Example 1: move1()

[{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

Here, we get what we want. Let’s see if our luck holds with the next example.

Example 2: move1({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

It works as desired! This time, the pattern with x and y being matched against {z:3} is not a problem, because they have their own local default values.

3.4.4 Conclusion: Default values are a feature of pattern parts

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.