Destructuring Regular Expression Matches in ECMAScript 2015
ECMAScript 2015 — previously known as ES6 — brings destructuring to JavaScript. Destructuring assignments allow you to decompose both arrays and objects of (partially) known structure using pattern matching:
let [first, second] = [1, 2];
// first = 1
// second = 2
The values of the righthand-side array are assigned to the two local variables first
and second
in the order they're defined.
It's perfectly legal to only match against some of the array elements (from left to right):
let [first] = [1, 2, 3, 4, 5];
// first = 1
If the list of variables is longer than the array itself, the unmatched variables receive the value undefined
, and no error is thrown:
let [first, second] = [1];
// first = 1
// second = undefined
As you can see, it's no problem to match against an array whose length isn't equal to the length of the variable list on the lefthand side. Matching against null
or undefined
, however, causes an error:
let [first] = null;
// Uncaught TypeError: Invalid attempt
// to destructure non-iterable instance
For a more detailed introduction and a comprehensive overview over destructuring both arrays and objects, I kindly refer you to Axel Rauschmayer's excellent post Destructuring and parameter handling in ECMAScript 6.
#Destructuring RegEx Matches
So why are destructuring assignments interesting for dealing with regular expressions? Because the exec
function matches a string against a specified pattern and returns its results as an array. That array always has the same structure:
- The first element represents the entire match with all its characters.
- The remaining elements contain the matches of all the capturing groups defined in the regular expression pattern.
Notice that, when the given string doesn't match the specified pattern, the exec
function returns null
.
Let's now assume we're given a date string and want to split it into its month, day, and year components. Here's a simple regular expression on which the exec
method is called with a sample date:
let datePattern = /^([a-z]+)\s+(\d+)\s*,\s+(\d{4})$/i;
let matchResult = datePattern.exec("June 24, 2015");
// matchResult = ["June 24, 2015", "June", "24", "2015"]
We can use a destructuring assignment to assign the entire match (the first element) and the captured values (all other elements) to local variables, like this:
let [match, month, day, year] = datePattern.exec("June 24, 2015") || [];
// match = "June 24, 2015"
// month = "June"
// day = "24"
// year = "2015"
Note that we're defaulting to an empty array using || []
when no match could be found. We do this because exec
can return null
against which we must not apply destructuring.
#Skipping and Grouping Elements
If we were only interested in the captured values, but not the entire match itself, we could skip the first element by not giving our pattern an identifier at that position:
let [, month, day, year] = datePattern.exec("June 24, 2015") || [];
// month = "June"
// day = "24"
// year = "2015"
We're still assigning all three captured values to separate local variables here. But what if we were interested in an array of all matches? We could use the rest operator, which is written as ...
, to return the remaining elements (those that haven't been matched against individual variables) as an array:
let [match, ...captures] = datePattern.exec("June 24, 2015") || [];
// match = "June 24, 2015"
// captures = ["June", "24", "2015"]
Quite elegant, don't you think?