Destructuring and parameter handling in ECMAScript 6
This blog post is outdated. Please read the following two chapters in “Exploring ES6”:
ECMAScript 6 (ES6) supports destructuring, a convenient way to extract values from data stored in (possibly nested) objects and arrays. This blog post describes how it works and gives examples of its usefulness. Additionally, parameter handling receives a significant upgrade in ES6: it becomes similar to and supports destructuring, which is why it is explained here, too.
Destructuring
In locations that receive data (such as the left-hand side of an assignment), destructuring lets you use patterns to extract parts of that data. In the following example, we use destructuring in a variable declaration (line (A)). It declares the variables f
and l
and assigns them the values 'Jane'
and 'Doe'
.
let obj = { first: 'Jane', last: 'Doe' };
let { first: f, last: l } = obj; // (A)
// f = 'Jane'; l = 'Doe'
Destructuring can be used in the following locations. Each time, x
is set to 'a'
.
// Variable declarations:
let [x] = ['a'];
const [x] = ['a'];
var [x] = ['a'];
// Assignments:
[x] = ['a'];
// Parameter definitions:
function f([x]) { ··· }
f(['a']);
Constructing versus extracting
To fully understand what destructuring is, let’s first examine its broader context. JavaScript has operations for constructing data:
let obj = {};
obj.first = 'Jane';
obj.last = 'Doe';
And it has operations for extracting data:
let f = obj.first;
let l = obj.last;
Note that we are using the same syntax that we have used for constructing.
There is nicer syntax for constructing – an object literal:
let obj = { first: 'Jane', last: 'Doe' };
Destructuring in ECMAScript 6 enables the same syntax for extracting data, where it is called an object pattern:
let { first: f, last: l } = obj;
Just as the object literal lets us create multiple properties at the same time, the object pattern lets us extract multiple properties at the same time.
You can also destructure arrays via patterns:
let [x, y] = ['a', 'b']; // x = 'a'; y = 'b'
We distinguish:
- Destructuring source: the data to be destructured. For example, the right-hand side of a destructuring assignment.
- Destructuring target: the pattern used for destructuring. For example, the left-hand side of a destructuring assignment.
Being selective with parts
If you destructure an object, you are free to mention only those properties that you are interested in:
let { x: x } = { x: 7, y: 3 }; // x = 7
If you destructure an array, you can choose to only extract a prefix:
let [x,y] = ['a', 'b', 'c']; // x='a'; y='b';
If a part has no match
Similarly to how JavaScript handles non-existent properties and array elements, destructuring silently fails if the target mentions a part that doesn’t exist in the source: the interior of the part is matched against undefined
. If the interior is a variable that means that the variable is set to undefined
:
let [x] = []; // x = undefined
let {prop:y} = {}; // y = undefined
Nesting
You can nest patterns arbitrarily deeply:
let obj = { a: [{ foo: 123, bar: 'abc' }, {}], b: true };
let { a: [{foo: f}] } = obj; // f = 123
How do patterns access the innards of values?
In an assignment pattern = someValue
, how does the pattern
acess what’s inside someValue
?
Object patterns coerce values to objects
The object pattern coerces destructuring sources to objects before accessing properties. That means that it works with primitive values:
let {length : len} = 'abc'; // len = 3
let {toString: s} = 123; // s = Number.prototype.toString
Failing to object-destructure a value
The coercion to object is not performed via Object()
, but via the internal operation ToObject()
. Object()
never fails:
> typeof Object('abc')
'object'
> var obj = {};
> Object(obj) === obj
true
> Object(undefined)
{}
> Object(null)
{}
ToObject()
throws a TypeError
if it encounters undefined
or null
. Therefore, the following destructurings fail, even before destructuring accesses any properties:
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
As a consequence, you can use the empty object pattern {}
to check whether a value is coercible to an object. As we have seen, only undefined
and null
aren’t:
({}) = [true, false]; // OK, arrays are coercible to objects
({}) = 'abc'; // OK, strings are coercible to objects
({}) = undefined; // TypeError
({}) = null; // TypeError
The parentheses around the object patterns are necessary because statements must not begin with curly braces in JavaScript.
Array patterns work with iterables
Array destructuring uses an iterator to get to the elements of a source. Therefore, you can array-destructure any value that is iterable. Let’s look at examples of iterable values.
Strings are iterable:
let [x,...y] = 'abc'; // x='a'; y=['b', 'c']
Don’t forget that the iterator over strings returns code points (“Unicode characters”, 21 bits), not code units (“JavaScript characters”, 16 bits). (For more information on Unicode, consult the chapter “Chapter 24. Unicode and JavaScript” in “Speaking JavaScript”.) For example:
let [x,y,z] = 'a\uD83D\uDCA9c'; // x='a'; y='\uD83D\uDCA9'; z='c'
You can’t access the elements of a set [4] via indices, but you can do so via an iterator. Therefore, array destructuring works for sets:
let [x,y] = new Set(['a', 'b']); // x='a'; y='b’;
The Set
iterator always returns elements in the order in which they were inserted, which is why the result of the previous destructuring is always the same.
Infinite sequences. Destructuring also works for iterators over infinite sequences. The generator function allNaturalNumbers()
returns an iterator that yields 0, 1, 2, etc.
function* allNaturalNumbers() {
for (let n = 0; ; n++) {
yield n;
}
}
The following destructuring extracts the first three elements of that infinite sequence.
let [x, y, z] = allNaturalNumbers(); // x=0; y=1; z=2
Failing to array-destructure a value
A value is iterable if it has a method whose key is Symbol.iterator
that returns an object. Array-destructuring throws a TypeError
if the value to be destructured isn’t iterable:
let x;
[x] = [true, false]; // OK, arrays are iterable
[x] = 'abc'; // OK, strings are iterable
[x] = { * [Symbol.iterator]() { yield 1 } }; // OK, iterable
[x] = {}; // TypeError, empty objects are not iterable
[x] = undefined; // TypeError, not iterable
[x] = null; // TypeError, not iterable
The TypeError
is thrown even before accessing elements of the iterable, which means that you can use the empty array pattern []
to check whether a value is iterable:
[] = {}; // TypeError, empty objects are not iterable
[] = undefined; // TypeError, not iterable
[] = null; // TypeError, not iterable
Default values
Default values are a feature of patterns:
- Each part of a pattern can optionally specify a default value.
- If the part has no match in the source, destructuring continues with the default value (if one exists) or
undefined
.
Let’s look at an example. In the following destructuring, the element at index 0 has no match on the right-hand side. Therefore, destructuring continues by matching x
against 3, which leads to x
being set to 3.
let [x=3, y] = []; // x = 3; y = undefined
You can also use default values in object patterns:
let {foo: x=3, bar: y} = {}; // x = 3; y = undefined
Default values are also used if a part does have a match and that match is undefined
:
let [x=1] = [undefined]; // x = 1
let {prop: y=2} = {prop: undefined}; // y = 2
The rationale for this behavior is explained later, in the section on parameter default values.
Default values are computed on demand
The default values themselves are only computed when they are needed. That is, this destructuring:
let {prop: y=someFunc()} = someValue;
is equivalent to:
let y;
if (someValue.prop === undefined) {
y = someFunc();
} else {
y = someValue.prop;
}
You can observe that if you use console.log()
:
> function log(x) { console.log(x); return 'YES' }
> let [x=log('hello')] = []; // x='YES'
hello
> let [x=log('hello')] = [123]; // x=123
In the second destructuring, the default value is not needed and log()
is not called.
Default values can refer to other variables in the pattern
A default value can refer to any variable, including another variable in the same pattern:
let x, y;
[x=3, y=x] = []; // x=3; y=3
[x=3, y=x] = [7]; // x=7; y=7
[x=3, y=x] = [7, 2]; // x=7; y=2
However, order matters: the variables x
and y
are declared from left to right and produce a ReferenceError
if they are accessed before their declaration.
Default values for patterns
So far we have only seen default values for variables, but you can also associate them with patterns:
let [{ prop: x } = {}] = [];
What does this mean? Recall the rule for default values:
If the part has no match in the source, destructuring continues with the default value […].
The element at index 0 has no match, which is why destructuring continues with:
let { prop: x } = {}; // x = undefined
You can more easily see why things work this way if you replace the pattern { prop: x }
with the variable pattern
:
let [pattern = {}] = [];
More complex default values. Let’s further explore default values for patterns. In the following example, we assign a value to x
via the default value { prop: 123 }
:
let [{ prop: x } = { prop: 123 }] = [];
Because the array element at index 0 has no match on the right-hand side, destructuring continues as follows and x
is set to 123.
let { prop: x } = { prop: 123 }; // x = 123
However, x
is not assigned a value in this manner if the right-hand side has an element at index 0, because then the default value isn’t triggered.
let [{ prop: x } = { prop: 123 }] = [{}];
In this case, destructuring continues with:
let { prop: x } = {}; // x = undefined
Thus, if you want x
to be 123 if either the object or the property is missing, you need to specify a default value for x
itself:
let [{ prop: x=123 } = {}] = [{}];
Here, destructuring continues as follows, independently of whether the right-hand side is [{}]
or []
.
let { prop: x=123 } = {}; // x = 123
More object destructuring features
Property value shorthands
Property value shorthands [2] are a feature of object literals: If the value of a property is provided via a variable whose name is the same as the key, you can omit the key. This works for destructuring, too:
let { x, y } = { x: 11, y: 8 }; // x = 11; y = 8
This declaration is equivalent to:
let { x: x, y: y } = { x: 11, y: 8 };
You can also combine property value shorthands with default values:
let { x, y = 1 } = {}; // x = undefined; y = 1
Computed property keys
Computed property keys [2] are another object literal feature that also works for destructuring: You can specify the key of a property via an expression, if you put it in square brackets:
const FOO = 'foo';
let { [FOO]: f } = { foo: 123 }; // f = 123
Computed property keys allow you to destructure properties whose keys are symbols [3]:
// Create and destructure a property whose key is a symbol
const KEY = Symbol();
let obj = { [KEY]: 'abc' };
let { [KEY]: x } = obj; // x = 'abc'
// Extract Array.prototype[Symbol.iterator]
let { [Symbol.iterator]: func } = [];
console.log(typeof func); // function
More array destructuring features
Elision
Elision lets you use the syntax of array “holes” to skip elements during destructuring:
let [,,x] = ['a', 'b', 'c', 'd']; // x = 'c'
Rest operator
The rest operator (...
) lets you extract the remaining elements of an array into an array. You can only use the operator as the last part inside an array pattern:
let [x, ...y] = ['a', 'b', 'c']; // x='a'; y=['b', 'c']
[Note: This operator extracts data. The same syntax (...
) is used by the spread operator, which constructs and is explained later.]
If the operator can’t find any elements, it matches its operand against the empty array. That is, it never produces undefined
or null
. For example:
let [x, y, ...z] = ['a']; // x='a'; y=undefined; z=[]
The operand of the rest operator doesn’t have to be a variable, you can use patterns, too:
let [x, ...[y, z]] = ['a', 'b', 'c'];
// x = 'a'; y = 'b'; z = 'c'
The rest operator triggers the following destructuring:
[y, z] = ['b', 'c']
You can assign to more than just variables
If you assign via destructuring, each variable part can be everything that is allowed on the left-hand side of a normal assignment, including a reference to a property (obj.prop
) and a reference to an array element (arr[0]
).
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] }) = { foo: 123, bar: true };
console.log(obj); // {prop:123}
console.log(arr); // [true]
You can also assign to object properties and array elements via the rest operator (...
):
let obj = {};
[first, ...obj.rest] = ['a', 'b', 'c'];
// first = 'a'; obj.rest = ['b', 'c']
If you declare variables via destructuring then you must use simple identifiers, you can’t refer to object properties and array elements.
Pitfalls of destructuring
There are two things to be mindful of when using destructuring.
Don’t start a statement with a curly brace
Because code blocks begin with a curly brace, statements must not begin with one. This is unfortunate when using object destructuring in an assignment:
{ a, b } = someObject; // SyntaxError
The work-around is to either put the pattern in parentheses or the complete expression:
({ a, b }) = someObject; // ok
({ a, b } = someObject); // ok
You can’t mix declaring and assigning to existing variables
Within a destructuring variable declaration, every variable in the source is declared. In the following example, we are trying to declare the variable b
and refer to the existing variable f
, which doesn’t work.
let f;
···
let { foo: f, bar: b } = someObject;
// During parsing (before running the code):
// SyntaxError: Duplicate declaration, f
The fix is to use a destructuring assignment and to declare b
beforehand:
let f;
···
let b;
({ foo: f, bar: b }) = someObject;
Examples of destructuring
Let’s start with a few smaller examples.
The for-of
loop [5] supports destructuring:
for (let [key, value] of map) {
console.log(key + ' is ' + value);
}
You can use destructuring to swap values. That is something that engines could optimize, so that no array would be created.
[a, b] = [b, a];
You can use destructuring to split an array:
let [first, ...rest] = ['a', 'b', 'c'];
// first = 'a'; rest = ['b', 'c']
Destructuring return values
Some built-in JavaScript operations return arrays. Destructuring helps with processing them:
let [all, year, month, day] =
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.exec('2999-12-31');
exec()
returns null
if the regular expression doesn’t match. Unfortunately, you can’t handle null
via default values, which is why you must use the Or operator (||
) in this case:
let [all, year, month, day] =
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.exec('2999-12-31') || [];
Multiple return values
To see the usefulness of multiple return values, let’s implement a function findElement(a, p)
that searches for the first element in the array a
for which the function p
returns true
. The question is: what should that function return? Sometimes one is interested in the element itself, sometimes in its index, sometimes in both. The following implementation does both.
function findElement(array, predicate) {
for (let [index, element] of array.entries()) { // (A)
if (predicate(element)) {
return { element, index }; // (B)
}
}
return { element: undefined, index: -1 };
}
In line (A), the array method entries()
returns an iterable over [index,element]
pairs. We destructure one pair per iteration. In line (B), we use property value shorthands to return the object { element: element, index: index }
.
In the following example, we use several ECMAScript features to write more concise code: An arrow functions helps us with defining the callback, destructuring and property value shorthands help us with handling the return value.
let arr = [7, 8, 6];
let {element, index} = findElement(arr, x => x % 2 === 0);
// element = 8, index = 1
Due to index
and element
also referring to property keys, the order in which we mention them doesn’t matter:
let {index, element} = findElement(···);
We have successfully handled the case of needing both index and element. What if we are only interested in one of them? It turns out that, thanks to ECMAScript 6, our implementation can take care of that, too. And the syntactic overhead compared to functions that support only elements or only indices is minimal.
let a = [7, 8, 6];
let {element} = findElement(a, x => x % 2 === 0);
// element = 8
let {index} = findElement(a, x => x % 2 === 0);
// index = 1
Each time, we only extract the value of the one property that we need.
Parameter handling
Parameter handling has been significantly upgraded in ECMAScript 6. It now supports parameter default values, rest parameters (varags) and destructuring. The new way of handling parameters is equivalent to destructuring the actual parameters via the formal parameters. That is, the following function call:
function f(«FORMAL_PARAMETERS») {
«CODE»
}
f(«ACTUAL_PARAMETERS»);
is equivalent to:
{
let [«FORMAL_PARAMETERS»] = [«ACTUAL_PARAMETERS»];
{
«CODE»
}
}
Let’s look at specific features next.
Parameter default values
ECMAScript 6 lets you specify default values for parameters:
function f(x, y=0) {
return [x, y];
}
Omitting the second parameter triggers the default value:
> f(1)
[1, 0]
> f()
[undefined, 0]
Watch out – undefined
triggers the default value, too:
> f(undefined, undefined)
[undefined, 0]
The default value is computed on demand, only when it is actually needed:
> const log = console.log.bind(console);
> function g(x=log('x'), y=log('y')) {return 'DONE'}
> g()
x
y
'DONE'
> g(1)
y
'DONE'
> g(1, 2)
'DONE'
Why does undefined
trigger default values?
It isn’t immediately obvious why undefined
should be interpreted as a missing parameter or a missing part of an object or array. The rationale for doing so is that it enables you to delegate the definition of default values. Let’s look at two examples.
In the first example (source: Rick Waldron’s TC39 meeting notes from 2012-07-24), we don’t have to define a default value in setOptions()
, we can delegate that task to setLevel()
.
function setLevel(newLevel = 0) {
light.intensity = newLevel;
}
function setOptions(options) {
// Missing prop returns undefined => use default
setLevel(options.dimmerLevel);
setMotorSpeed(options.speed);
···
}
setOptions({speed:5});
In the second example, square()
doesn’t have to define a default for x
, it can delegate that task to multiply()
:
function multiply(x=1, y=1) {
return x * y;
}
function square(x) {
return multiply(x, x);
}
Default values further entrench the role of undefined
as indicating that something doesn’t exist, versus null
indicating emptiness.
Referring to other variables in default values
Within a parameter default value, you can refer to any variable, including other parameters:
function foo(x=3, y=x) { ··· }
foo(); // x=3; y=3
foo(7); // x=7; y=7
foo(7, 2); // x=7; y=2
However, order matters: parameters are declared from left to right and within a default value, you get a ReferenceError
if you access a parameter that hasn’t been declared, yet.
Default values exist in their own scope, which is between the “outer” scope surrounding the function and the “inner” scope of the function body. Therefore, you can’t access inner variables from the default values:
let x = 'outer';
function foo(a = x) {
let x = 'inner';
console.log(a); // outer
}
If there were no outer x
in the previous example, the default value x
would produce a ReferenceError
.
Rest parameters
Putting the rest operator (...
) in front of the last formal parameter means that it will receive all remaining actual parameters in an array.
function f(x, ...y) {
···
}
f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c']
If there are no remaining parameters, the rest parameter will be set to the empty array:
f(); // x = undefined; y = []
No more arguments
!
Rest parameters can completely replace JavaScript’s infamous special variable arguments
. They have the advantage of always being arrays:
// ECMAScript 5: arguments
function logAllArguments() {
for (var i=0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
// ECMAScript 6: rest parameter
function logAllArguments(...args) {
for (let arg of args) {
console.log(arg);
}
}
One interesting feature of arguments
is that you can have normal parameters and an array of all parameters at the same time:
function foo(x, y) {
console.log('Arity: '+arguments.length);
···
}
You can avoid arguments
in such cases if you combine a rest parameter with array destructuring. The resulting code is longer, but more explicit:
function foo(...args) {
let [x, y] = args;
console.log('Arity: '+args.length);
···
}
Note that arguments
is iterable [5] in ECMAScript 6, which means that you can use for-of
and the spread operator:
> (function () { return typeof arguments[Symbol.iterator] }())
'function'
> (function () { return Array.isArray([...arguments]) }())
true
Simulating named parameters
When calling a function (or method) in a programming language, you must map the actual parameters (specified by the caller) to the formal parameters (of a function definition). There are two common ways to do so:
Positional parameters are mapped by position. The first actual parameter is mapped to the first formal parameter, the second actual to the second formal, and so on.
Named parameters use names (labels) to perform the mapping. Names are associated with formal parameters in a function definition and label actual parameters in a function call. It does not matter in which order named parameters appear, as long as they are correctly labeled.
Named parameters have two main benefits: they provide descriptions for arguments in function calls and they work well for optional parameters. I’ll first explain the benefits and then show you how to simulate named parameters in JavaScript via object literals.
Named Parameters as Descriptions
As soon as a function has more than one parameter, you might get confused about what each parameter is used for. For example, let’s say you have a function, selectEntries()
, that returns entries from a database. Given the function call:
selectEntries(3, 20, 2);
what do these two numbers mean? Python supports named parameters, and they make it easy to figure out what is going on:
# Python syntax
selectEntries(start=3, end=20, step=2)
Optional Named Parameters
Optional positional parameters work well only if they are omitted at the end. Anywhere else, you have to insert placeholders such as null
so that the remaining parameters have correct positions.
With optional named parameters, that is not an issue. You can easily omit any of them. Here are some examples:
# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()
Simulating Named Parameters in JavaScript
JavaScript does not have native support for named parameters like Python and many other languages. But there is a reasonably elegant simulation: name parameters via an object literal, passed as a single actual parameter. When you use this technique, an invocation of selectEntries()
looks like:
selectEntries({ start: 3, end: 20, step: 2 });
The function receives an object with the properties start
, end
, and step
. You can omit any of them:
selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();
In ECMAScript 5, you’d implement selectEntries()
as follows:
function selectEntries(options) {
options = options || {};
var start = options.start || 0;
var end = options.end || getDbLength();
var step = options.step || 1;
···
}
In ECMAScript 6, you can use destructuring, which looks like this:
function selectEntries({ start=0, end=-1, step=1 }) {
···
};
If you call selectEntries()
with zero arguments, the destructuring fails, because you can’t match an object pattern against undefined
. That can be fixed via a default value. In the following code, the object pattern is matched against {}
if there isn’t at least one argument.
function selectEntries({ start=0, end=-1, step=1 } = {}) {
···
};
You can also combine positional parameters with named parameters. It is customary for the latter to come last:
someFunc(posArg1, { namedArg1: 7, namedArg2: true });
In principle, JavaScript engines could optimize this pattern so that no intermediate object is created, because both the object literals at the call sites and the object patterns in the function definitions are static.
Note: In JavaScript, the pattern for named parameters shown here is sometimes called options or option object (e.g., by the jQuery documentation).
Pitfall: destructuring a single arrow function parameter
Arrow functions have a special single-parameter version where no parentheses are needed:
// OK, but parentheses around `x` are not necessary:
someArray.map((x) => x * 2)
// Special paren-less version:
someArray.map(x => x * 2)
The single-parameter version does not support destructuring:
someArray.map([x,y] => x+y) // SyntaxError
someArray.map(([x,y]) => x+y) // OK
Examples of parameter handling
forEach() and destructuring
You will probably mostly use the for-of
loop in ECMAScript 6, but the array method forEach()
also profits from destructuring. Or rather, its callback does.
First example: destructuring the arrays in an array.
let items = [ ['foo', 3], ['bar', 9] ];
items.forEach(([word, count]) => {
console.log(word+' '+count);
});
Second example: destructuring the objects in an array.
let items = [
{ word:'foo', count:3 },
{ word:'bar', count:9 },
];
items.forEach(({word, count}) => {
console.log(word+' '+count);
});
Transforming maps
An ECMAScript 6 Map [4] doesn’t have a method map()
(like arrays). Therefore, one has to:
- Convert it to an array of
[key,value]
pairs. map()
the array.- Convert the result back to a map.
This looks as follows.
let map0 = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
]);
let map1 = new Map(
[...map0] // step 1
.map(([k, v]) => [k*2, '_'+v]) // step 2
); // step 3
// Resulting map: {2 -> '_a', 4 -> '_b', 6 -> '_c'}
Handling an array returned via a Promise
The tool method Promise.all()
[6] works as follows:
- Input: an array of Promises.
- Output: a Promise that resolves to an array as soon as the last input Promise is resolved. The array contains the resolutions of the input Promises.
Destructuring helps with handling the array that the result of Promise.all()
resolves to:
let urls = [
'http://example.com/foo.html',
'http://example.com/bar.html',
'http://example.com/baz.html',
];
Promise.all(urls.map(downloadUrl))
.then(([fooStr, barStr, bazStr]) => {
···
});
// This function returns a Promise that resolves to
// a string (the text)
function downloadUrl(url) {
return fetch(url).then(request => request.text());
}
fetch()
is a Promise-based version of XMLHttpRequest
. It is part of the Fetch standard.
Required parameters
In ECMAScript 5, you have a few options for ensuring that a required parameter has been provided, which are all quite clumsy:
function foo(mustBeProvided) {
if (arguments.length < 1) {
throw new Error();
}
if (! (0 in arguments)) {
throw new Error();
}
if (mustBeProvided === undefined) {
throw new Error();
}
···
}
In ECMAScript 6, you can (ab)use default parameter values to achieve more concise code (credit: idea by Allen Wirfs-Brock):
/**
* Called if a parameter is missing and
* the default value is evaluated.
*/
function mandatory() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = mandatory()) {
return mustBeProvided;
}
Interaction:
> foo()
Error: Missing parameter
> foo(123)
123
Enforcing a maximum arity
This section presents three approaches to enforcing a maximum arity. The running example is a function f
whose maximum arity is 2 – if a caller provides more than 2 parameters, an error should be thrown.
The first approach collects all actual parameters in the formal rest parameter args
and checks its length.
function f(...args) {
if (args.length > 2) {
throw new Error();
}
// Extract the real parameters
let [x, y] = args;
}
The second approach relies on unwanted actual parameters appearing in the formal rest parameter extra
.
function f(x, y, ...extra) {
if (extra.length > 0) {
throw new Error();
}
}
The third approach uses a sentinel value that is gone if there is a third parameter. One caveat is that the default value OK
is also triggered if there is a third parameter whose value is undefined
.
const OK = Symbol();
function f(x, y, arity=OK) {
if (arity !== OK) {
throw new Error();
}
}
Sadly, each one of these approaches introduces significant visual and conceptual clutter. I’m tempted to recommend checking arguments.length
, but I also want arguments
to go away.
function f(x, y) {
if (arguments.length > 2) {
throw new Error();
}
}
The spread operator (...
)
The spread operator (...
) is the opposite of the rest operator: Where the rest operator extracts arrays, the spread operator turns the elements of an array into the arguments of a function call or into elements of another array.
Spreading into function and method calls
Math.max()
is a good example for demonstrating how the spread operator works in method calls. Math.max(x1, x2, ···)
returns the argument whose value is greatest. It accepts an arbitrary number of arguments, but can’t be applied to arrays. The spread operator fixes that:
> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
In contrast to the rest operator, you can use the spread operator anywhere in a sequence of parts:
> Math.max(-1, ...[-1, 5, 11], 3)
11
Another example is JavaScript not having a way to destructively append the elements of one array to another one. However, arrays do have the method push(x1, x2, ···)
, which appends all of its arguments to its receiver. The following code shows how you can use push()
to append the elements of arr2
to arr1
.
let arr1 = ['a', 'b'];
let arr2 = ['c', 'd'];
arr1.push(...arr2);
// arr1 is now ['a', 'b', 'c', 'd']
Spreading into constructors
In addition to function and method calls, the spread operator also works for constructor calls:
new Date(...[1912, 11, 24]) // Christmas Eve 1912
That is something that is difficult to achieve in ECMAScript 5.
Spreading into arrays
The spread operator can also be used inside arrays:
> [1, ...[2,3], 4]
[1, 2, 3, 4]
That gives you a convenient way to concatenate arrays:
let x = ['a', 'b'];
let y = ['c'];
let z = ['d', 'e'];
let arr = [...x, ...y, ...z]; // ['a', 'b', 'c', 'd', 'e']
Converting iterable or array-like objects to arrays
The spread operator lets you convert any iterable object to an array:
let arr = [...someIterableObject];
Let’s convert a set [4] to an array:
let set = new Set([11, -1, 6]);
let arr = [...set]; // [11, -1, 6]
Your own iterable objects [5] can be converted to arrays in the same manner:
let obj = {
* [Symbol.iterator]() {
yield 'a';
yield 'b';
yield 'c';
}
};
let arr = [...obj]; // ['a', 'b', 'c']
Note that, just like the for-of
loop, the spread operator only works for iterable objects. Most important objects are iterable: arrays, maps, sets and arguments
. Most DOM data structures will also eventually be iterable.
Should you ever encounter something that is not iterable, but array-like (indexed elements plus a property length
), you can use Array.from()
[7] to convert it to an array:
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ECMAScript 5:
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ECMAScript 6:
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
// TypeError: Cannot spread non-iterable object.
let arr3 = [...arrayLike];
Further reading:
- Using ECMAScript 6 today [a very early draft of my book on ECMAScript 6]
- ECMAScript 6: new OOP features besides classes
- Symbols in ECMAScript 6
- ECMAScript 6: maps and sets
- Iterators and generators in ECMAScript 6
- ECMAScript 6 promises (2/2): the API
- ECMAScript 6’s new array methods
Comments
Post a Comment