Functions with Default Parameter Values
Functions in JavaScript are unique in that they allow any number of parameters to be passed, regardless of the number of parameters declared in the function definition. This allows you to define functions that can handle different numbers of parameters, often by just filling in default values when parameters aren’t provided. This section covers how default parameters work both in and prior to ECMAScript 6, along with some important information on the arguments
object, using expressions as parameters, and another TDZ.
Simulating Default Parameter Values in ECMAScript 5
In ECMAScript 5 and earlier, you would likely use the following pattern to create a function with default parameters values:
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
// the rest of the function
}
In this example, both timeout
and callback
are actually optional because they are given a default value if a parameter isn’t provided. The logical OR operator (||
) always returns the second operand when the first is falsy. Since named function parameters that are not explicitly provided are set to undefined
, the logical OR operator is frequently used to provide default values for missing parameters. There is a flaw with this approach, however, in that a valid value for timeout
might actually be 0
, but this would replace it with 2000
because 0
is falsy.
In that case, a safer alternative is to check the type of the argument using typeof
, as in this example:
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function() {};
// the rest of the function
}
While this approach is safer, it still requires a lot of extra code for a very basic operation. Popular JavaScript libraries are filled with similar patterns, as this represents a common pattern.
Default Parameter Values in ECMAScript 6
ECMAScript 6 makes it easier to provide default values for parameters by providing initializations that are used when the parameter isn’t formally passed. For example:
function makeRequest(url, timeout = 2000, callback = function() {}) {
// the rest of the function
}
This function only expects the first parameter to always be passed. The other two parameters have default values, which makes the body of the function much smaller because you don’t need to add any code to check for a missing value.
When makeRequest()
is called with all three parameters, the defaults are not used. For example:
// uses default timeout and callback
makeRequest("/foo");
// uses default callback
makeRequest("/foo", 500);
// doesn't use defaults
makeRequest("/foo", 500, function(body) {
doSomething(body);
});
ECMAScript 6 considers url
to be required, which is why "/foo"
is passed in all three calls to makeRequest()
. The two parameters with a default value are considered optional.
It’s possible to specify default values for any arguments, including those that appear before arguments without default values in the function declaration. For example, this is fine:
function makeRequest(url, timeout = 2000, callback) {
// the rest of the function
}
In this case, the default value for timeout
will only be used if there is no second argument passed in or if the second argument is explicitly passed in as undefined
, as in this example:
// uses default timeout
makeRequest("/foo", undefined, function(body) {
doSomething(body);
});
// uses default timeout
makeRequest("/foo");
// doesn't use default timeout
makeRequest("/foo", null, function(body) {
doSomething(body);
});
In the case of default parameter values, a value of null
is considered to be valid, meaning that in the third call to makeRequest()
, the default value for timeout
will not be used.
How Default Parameter Values Affect the arguments Object
Just keep in mind that the behavior of the arguments
object is different when default parameter values are present. In ECMAScript 5 nonstrict mode, the arguments
object reflects changes in the named parameters of a function. Here’s some code that illustrates how this works:
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
This outputs:
true
true
true
true
The arguments
object is always updated in nonstrict mode to reflect changes in the named parameters. Thus, when first
and second
are assigned new values, arguments[0]
and arguments[1]
are updated accordingly, making all of the ===
comparisons resolve to true
.
ECMAScript 5’s strict mode, however, eliminates this confusing aspect of the arguments
object. In strict mode, the arguments
object does not reflect changes to the named parameters. Here’s the mixArgs()
function again, but in strict mode:
function mixArgs(first, second) {
"use strict";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
The call to mixArgs()
outputs:
true
true
false
false
This time, changing first
and second
doesn’t affect arguments
, so the output behaves as you’d normally expect it to.
The arguments
object in a function using ECMAScript 6 default parameter values, however, will always behave in the same manner as ECMAScript 5 strict mode, regardless of whether the function is explicitly running in strict mode. The presence of default parameter values triggers the arguments
object to remain detached from the named parameters. This is a subtle but important detail because of how the arguments
object may be used. Consider the following:
// not in strict mode
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");
This outputs:
1
true
false
false
false
In this example, arguments.length
is 1 because only one argument was passed to mixArgs()
. That also means arguments[1]
is undefined
, which is the expected behavior when only one argument is passed to a function. That means first
is equal to arguments[0]
as well. Changing first
and second
has no effect on arguments
. This behavior occurs in both nonstrict and strict mode, so you can rely on arguments
to always reflect the initial call state.
Default Parameter Expressions
Perhaps the most interesting feature of default parameter values is that the default value need not be a primitive value. You can, for example, execute a function to retrieve the default parameter value, like this:
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
Here, if the last argument isn’t provided, the function getValue()
is called to retrieve the correct default value. Keep in mind that getValue()
is only called when add()
is called without a second parameter, not when the function declaration is first parsed. That means if getValue()
were written differently, it could potentially return a different value. For instance:
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7
In this example, value
begins as five and increments each time getValue()
is called. The first call to add(1)
returns 6, while the second call to add(1)
returns 7 because value
was incremented. Because the default value for second
is only evaluated when the function is called, changes to that value can be made at any time.
W> Be careful when using function calls as default parameter values. If you forget the parentheses, such as second = getValue
in the last example, you are passing a reference to the function rather than the result of the function call.
This behavior introduces another interesting capability. You can use a previous parameter as the default for a later parameter. Here’s an example:
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
In this code, the parameter second
is given a default value of first
, meaning that passing in just one argument leaves both arguments with the same value. So add(1, 1)
returns 2 just as add(1)
returns 2. Taking this a step further, you can pass first
into a function to get the value for second
as follows:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
This example sets second
equal to the value returned by getValue(first)
, so while add(1, 1)
still returns 2, add(1)
returns 7 (1 + 6).
The ability to reference parameters from default parameter assignments works only for previous arguments, so earlier arguments do not have access to later arguments. For example:
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // throws error
The call to add(undefined, 1)
throws an error because second
is defined after first
and is therefore unavailable as a default value. To understand why that happens, it’s important to revisit temporal dead zones.
Default Parameter Value Temporal Dead Zone
Chapter 1 introduced the temporal dead zone (TDZ) as it relates to let
and const
, and default parameter values also have a TDZ where parameters cannot be accessed. Similar to a let
declaration, each parameter creates a new identifier binding that can’t be referenced before initialization without throwing an error. Parameter initialization happens when the function is called, either by passing a value for the parameter or by using the default parameter value.
To explore the default parameter value TDZ, consider this example from “Default Parameter Expressions” again:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
The calls to add(1, 1)
and add(1)
effectively execute the following code to create the first
and second
parameter values:
// JavaScript representation of call to add(1, 1)
let first = 1;
let second = 1;
// JavaScript representation of call to add(1)
let first = 1;
let second = getValue(first);
When the function add()
is first executed, the bindings first
and second
are added to a parameter-specific TDZ (similar to how let
behaves). So while second
can be initialized with the value of first
because first
is always initialized at that time, the reverse is not true. Now, consider this rewritten add()
function:
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // throws error
The calls to add(1, 1)
and add(undefined, 1)
in this example now map to this code behind the scenes:
// JavaScript representation of call to add(1, 1)
let first = 1;
let second = 1;
// JavaScript representation of call to add(undefined, 1)
let first = second;
let second = 1;
In this example, the call to add(undefined, 1)
throws an error because second
hasn’t yet been initialized when first
is initialized. At that point, second
is in the TDZ and therefore any references to second
throw an error. This mirrors the behavior of let
bindings discussed in Chapter 1.
I> Function parameters have their own scope and their own TDZ that is separate from the function body scope. That means the default value of a parameter cannot access any variables declared inside the function body.