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:

  1. function makeRequest(url, timeout, callback) {
  2. timeout = timeout || 2000;
  3. callback = callback || function() {};
  4. // the rest of the function
  5. }

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:

  1. function makeRequest(url, timeout, callback) {
  2. timeout = (typeof timeout !== "undefined") ? timeout : 2000;
  3. callback = (typeof callback !== "undefined") ? callback : function() {};
  4. // the rest of the function
  5. }

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:

  1. function makeRequest(url, timeout = 2000, callback = function() {}) {
  2. // the rest of the function
  3. }

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:

  1. // uses default timeout and callback
  2. makeRequest("/foo");
  3. // uses default callback
  4. makeRequest("/foo", 500);
  5. // doesn't use defaults
  6. makeRequest("/foo", 500, function(body) {
  7. doSomething(body);
  8. });

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:

  1. function makeRequest(url, timeout = 2000, callback) {
  2. // the rest of the function
  3. }

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:

  1. // uses default timeout
  2. makeRequest("/foo", undefined, function(body) {
  3. doSomething(body);
  4. });
  5. // uses default timeout
  6. makeRequest("/foo");
  7. // doesn't use default timeout
  8. makeRequest("/foo", null, function(body) {
  9. doSomething(body);
  10. });

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:

  1. function mixArgs(first, second) {
  2. console.log(first === arguments[0]);
  3. console.log(second === arguments[1]);
  4. first = "c";
  5. second = "d";
  6. console.log(first === arguments[0]);
  7. console.log(second === arguments[1]);
  8. }
  9. mixArgs("a", "b");

This outputs:

  1. true
  2. true
  3. true
  4. 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:

  1. function mixArgs(first, second) {
  2. "use strict";
  3. console.log(first === arguments[0]);
  4. console.log(second === arguments[1]);
  5. first = "c";
  6. second = "d"
  7. console.log(first === arguments[0]);
  8. console.log(second === arguments[1]);
  9. }
  10. mixArgs("a", "b");

The call to mixArgs() outputs:

  1. true
  2. true
  3. false
  4. 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:

  1. // not in strict mode
  2. function mixArgs(first, second = "b") {
  3. console.log(arguments.length);
  4. console.log(first === arguments[0]);
  5. console.log(second === arguments[1]);
  6. first = "c";
  7. second = "d"
  8. console.log(first === arguments[0]);
  9. console.log(second === arguments[1]);
  10. }
  11. mixArgs("a");

This outputs:

  1. 1
  2. true
  3. false
  4. false
  5. 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:

  1. function getValue() {
  2. return 5;
  3. }
  4. function add(first, second = getValue()) {
  5. return first + second;
  6. }
  7. console.log(add(1, 1)); // 2
  8. 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:

  1. let value = 5;
  2. function getValue() {
  3. return value++;
  4. }
  5. function add(first, second = getValue()) {
  6. return first + second;
  7. }
  8. console.log(add(1, 1)); // 2
  9. console.log(add(1)); // 6
  10. 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:

  1. function add(first, second = first) {
  2. return first + second;
  3. }
  4. console.log(add(1, 1)); // 2
  5. 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:

  1. function getValue(value) {
  2. return value + 5;
  3. }
  4. function add(first, second = getValue(first)) {
  5. return first + second;
  6. }
  7. console.log(add(1, 1)); // 2
  8. 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:

  1. function add(first = second, second) {
  2. return first + second;
  3. }
  4. console.log(add(1, 1)); // 2
  5. 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:

  1. function getValue(value) {
  2. return value + 5;
  3. }
  4. function add(first, second = getValue(first)) {
  5. return first + second;
  6. }
  7. console.log(add(1, 1)); // 2
  8. 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:

  1. // JavaScript representation of call to add(1, 1)
  2. let first = 1;
  3. let second = 1;
  4. // JavaScript representation of call to add(1)
  5. let first = 1;
  6. 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:

  1. function add(first = second, second) {
  2. return first + second;
  3. }
  4. console.log(add(1, 1)); // 2
  5. 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:

  1. // JavaScript representation of call to add(1, 1)
  2. let first = 1;
  3. let second = 1;
  4. // JavaScript representation of call to add(undefined, 1)
  5. let first = second;
  6. 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.