Defining Default Values for Parameters

Parameters can take default values that step in for any missing arguments. We can benefit from default parameters in one of three ways:

  • As a user of a function, we don’t have to pass in a value for a parameter if the value we intend to send is the same as the sensible default chosen by the creator of the function—resulting in less work and reduced noise/clutter in code.

  • As the author of a function, we can evolve the function signature more freely, to add a new parameter, without breaking existing code.

  • We can compensate for the lack of function overloading in JavaScript. Many modern languages provide function overloading, but JavaScript does not. With default parameters, the caller may pass a different number of parameters, giving the illusion of using overloaded functions.

Let’s explore the default values for parameters feature with an example to evolve a function.

Suppose we want to implement a function to sort a given array of books based on their titles. We can readily write that function, like so:

 const​ sortByTitle = ​function​(books) {
 const​ byTitle = ​function​(book1, book2) {
 return​ book1.title.localeCompare(book2.title);
  };
 
 return​ books.slice().sort(byTitle);
 };

Within the sortByTitle() function we sort the given books array, but instead of calling sort() directly on the array we first use slice() and then sort(). The reason for not using sort() directly is that it will modify the array on which it is called—changing the input given to a function is a poor programming practice. The slice() function makes a copy of the given array and the sort() function then sorts the copy, thus not affecting the original array given as input.

Let’s call the sortByTitle() function with some sample data.

 const​ books = [
  { title: ​'Who Moved My Cheese'​ },
  { title: ​'Great Expectations'​ },
  { title: ​'The Power of Positive Thinking'​ }
 ];
 
 console.log(sortByTitle(books));

The output of this call is the books sorted by title:

 [ { title: 'Great Expectations' },
  { title: 'The Power of Positive Thinking' },
  { title: 'Who Moved My Cheese' } ]

Now suppose after a few weeks we’re asked to enhance the function. While the users of our function mostly sort the books in the ascending order of the title, sometimes they may want to sort them in the descending order. We could write a new function for that, but that will result in duplication of significant code. If we change the function to take in an additional parameter, that may break existing code.

Technically, if we suddenly throw in a new extra parameter, the existing code will actually not be affected—at least not immediately. When the call is made, the value for the newly added parameter will come in as undefined. The code will then have to do undefined checks on the value of that new parameter—friends don’t let friends write code like that. Furthermore, when the user of the function revisits he or she will be quite confused and start providing the necessary parameter; that’s no fun either. The solution: default parameters.

Let’s rework the function to use a default parameter:

 const​ sortByTitle = ​function​(books, ascending = ​true​) {
 const​ multiplier = ascending ? 1 : -1;
 
 const​ byTitle = ​function​(book1, book2) {
 return​ book1.title.localeCompare(book2.title) * multiplier;
  };
 
 return​ books.slice().sort(byTitle);
 };

We added a second parameter, ascending, but gave it a default value of true. If the caller does not provide a value for this parameter, then a value of true is assumed for it. If a value is given, however, then that value is used.

Within the function, we create a local variable named multiplier, which holds a value of 1 if the value of ascending is true and -1 otherwise. We used the ternary operator, to keep the code concise, for this evaluation of multiplier. The localeCompare returns a positive number, a zero, or a negative number depending on whether the first value is greater, equal to, or smaller, respectively, than the second value. The multiplier of 1 will preserve that ordering whereas a value of -1 reverses it.

Let’s repeat the old call to the function, but also add a new call to pass the value of false to the newly added parameter.

 console.log(sortByTitle(books));
 console.log(sortByTitle(books, ​false​));

Since the first call is not passing any value for the second argument, the default value kicks in for the second parameter and the sorting happens in the ascending order. However, since a value is passed to the second argument in the second call, that value will appear for the second parameter. Let’s quickly take a look at the output of these two calls:

 [ { title: 'Great Expectations' },
  { title: 'The Power of Positive Thinking' },
  { title: 'Who Moved My Cheese' } ]
 [ { title: 'Who Moved My Cheese' },
  { title: 'The Power of Positive Thinking' },
  { title: 'Great Expectations' } ]

In this example we saw how the default parameter helped to evolve the function. However, it does not have to be an afterthought. We can also proactively design our functions with sensible default values. Such design decisions may help the users of the functions to pass only essential arguments and rely on the defaults for the obvious and intuitive values that they don’t care to customize.

The previous example showed how we can pass a custom value for the default parameter or simply omit it. If we omit it, the parameter takes on the default value provided in the function declaration. That is pretty straightforward, but our experience tells us it can’t be that simple—there has to be more things to it to make this at least a tad complex. Well, of course, as we’ll see next.

Multiple Default Parameters

A function can have any number of default parameters. For example, let’s define a function with one regular parameter and two default parameters.

 const​ fetchData = ​function​(
  id,
  location = { host: ​'localhost'​, port: 443 },
  uri = ​'employees'​) {
 
  console.log(​'Fetch data from https://'​ +
  location.host + ​':'​ + location.port + ​'/'​ + uri);
 };

The caller of this function may pass three arguments, pass two arguments and leave out the value of the last parameter, or pass one argument and leave out both of the default parameters.

 fetchData(1, { host: ​'agiledeveloper'​, port: 404 }, ​'books'​);
 fetchData(1, { host: ​'agiledeveloper'​, port: 404 });
 fetchData(2);

In the first call, the given values were used for all three parameters, as we see in the following output. In the second call, the default value was used for the uri parameter. In the third call, both uri and location received the default values.

 Fetch data from https://agiledeveloper:404/books
 Fetch data from https://agiledeveloper:404/employees
 Fetch data from https://localhost:443/employees

That’s simple and straightforward too, but we’re not done with this topic yet.

Passing undefined

Pass what?! Yep, that nasty undefined.

What if the calling code of the fetchData() function wants to pass a value for the uri parameter but not for location—it wants to use the default value for that.

We can think of two possibilities, neither of which is true in JavaScript:

  • Don’t permit that. Some languages that provide default parameters follow this rule. They will require values for all parameters to the left if you specify the values for a default parameter. In other words, in these languages, if we choose to use the default value for a parameter, we are forced to use default values for all parameters that follow. JavaScript does not have that rule.

  • Require an empty parameter, like for example, fetchData(3,, ’whatever’). Thankfully, JavaScript does not allow that—imagine a function call like foo(1,,, 7,,, 20), delivered straight from hell.

But JavaScript permits passing undefined, and that has a special horror effect.

Here are the JavaScript rules:

  • If a good value is passed to the default parameter, then that given value is used.

  • If null is passed, then the value for the parameter is null—fair deal. So, don’t pass null; after all, null is a smell.

  • If undefined is passed, however, then the default value is given to the parameter in place of undefined.

Due to this feature, we may call the fetchData() function to provide a value for uri and use the default value for the location parameter, like so:

 fetchData(3, ​undefined​, ​'books'​);

That call will give this result:

 Fetch data from https://localhost:443/books

The issue is that undefined does not quite reveal the intention. It may not be too bad if we used it sparingly, but certainly avoid something like foo(1, undefined, undefined, 7, undefined, undefined, 20) because it’s not easy to read.

Passing undefined arguments in method calls is rather unpleasant, but the feature of mapping undefined to default values is quite powerful. When using the spread operator or when using destructuring—which we see in Chapter 6, Literals and Destructuring—missing values in the arguments list will turn up as undefined and thus will readily map to the default values on the parameter side.

Position of Default Parameters

The fact that passing undefined is inferred as a signal to choose the default value for a parameter leads to another implication. Unlike languages that require default parameters to be in trailing positions, JavaScript does not care. Here’s a function with default parameters in arbitrary positions.

 const​ badFetchData = ​function​(
  location = { host: ​'localhost'​, port: 443 },
  id,
  uri = ​'employees'​) {
 
  console.log(​'Fetch data from https://'​ +
  location.host + ​':'​ + location.port + ​'/'​ + uri);
 };

The badFetchData() function also has three parameters, like the fetchData() function; however, the first and last parameters have default values while the middle one is a regular parameter. Since a value for id is expected, the users of the function may either have to give a value for location or use the unpleasant undefined as an argument, like so:

 badFetchData(​undefined​, 4, ​'magazines'​);

Mixing default parameters and regular parameters is poor design. JavaScript will not stop us from doing that, but our wisdom should. As a good practice keep all the default parameters trailing.

Expressions as Default Values

The default values are not limited to literals. Expressions are welcome as well, and that’s quite powerful—kudos to JavaScript for that.

In the following code, the fileTax() function needs a date of filing. If the caller does not provide it, the current date of execution is assumed as the default value.

 const​ fileTax = ​function​(papers, dateOfFiling = ​new​ Date()) {
  console.log(​'dateOfFiling: '​ + dateOfFiling.getFullYear());
 };

Let’s call this function to verify the behavior of the expression in the default value.

 fileTax(​'stuff'​, ​new​ Date(​'2016-12-31'​));
 fileTax(​'stuff'​);

In the first call, we pass the last day of the year 2016 as the second argument. However, in the second call we leave it out. As we see from the output, the expression was evaluated on call and the year 2018 is used:

 dateOfFiling: 2016
 dateOfFiling: 2018

The value of the expression used for the default value is evaluated at the time of the call.

The expression that evaluates a default value for a parameter may use other parameters to the left. This gives the ability to compute the default value for a parameter based on other parameters’ values, default or not.

Let’s put that idea into a concrete—again a tax-related—example:

 const​ computeTax = ​function​(amount,
  stateTax = 15, localTax = stateTax * .10) {
  console.log(​'stateTax: '​ + stateTax + ​' localTax: '​ + localTax);
 };

The computeTax() function takes a required parameter followed by stateTax, which has a default value. The last parameter, localTax, uses an expression to compute the default value. The expression computes the value based on the current value of stateTax.

If the user gives a value for stateTax and localTax, then those given values are used and neither the default value nor the expression has any effect. If the user gives a value for the stateTax only, then the localTax is computed based on the given value for stateTax. If both stateTax and localTax are left out by the caller, then localTax is computed based on the default value for stateTax.

Let’s see these in action with a few calls to the function:

 computeTax(100, 10, 2);
 computeTax(100, 10);
 computeTax(100);

Let’s glance at the output:

 stateTax: 10 localTax: 2
 stateTax: 10 localTax: 1
 stateTax: 15 localTax: 1.5

The first call to computeTax() provides arguments for all three parameters and so the defaults don’t have any effect. In the second call, the value for localTax is one-tenth of the value given for stateTax. In the last call, it is one-tenth of the default value for stateTax.

Don’t use a parameter that’s to the right in the computation of the default value for a parameter. But if you grew up as one of those curious kids who won’t take “no” for an answer, don’t change now; go ahead and try.

Try changing

 const​ computeTax = ​function​(amount,
  stateTax = 15, localTax = stateTax * .10) {

to

 const​ computeTax = ​function​(amount,
  stateTax = localTax * 10, localTax = stateTax * .10) {

Then, with a wicked smile, run the code, with the three calls to computeTax(). Remember, JavaScript is both naughty and lazy. On the naughty side, it does not complain much; it simply likes to watch the code crash and burn. On the lazy side, it will wait until the last minute to tell you oops.

There will be no errors on the first two calls; after all, we provided the value for stateTax and are not using the default value—why complain about what is not used? However, in the last call where we are not passing stateTax, it will complain, as we see in the output:

 stateTax: 10 localTax: 2
 stateTax: 10 localTax: 1
 ...
  stateTax = localTax * 10, localTax = stateTax * .10) {
  ^
 
 ReferenceError: localTax is not defined

Programmers used to statically typed languages and rich compile-time checking will find this kind of behavior frustrating. Good automated testing of JavaScript code is highly critical to avoid runtime blowups—see Test-Driving JavaScript Applications [Sub16].

We’ve seen in isolation two features related to parameters. The question is, do they play well with each other? We’ll answer that question next.

Interplay of the Default and Rest Parameters

We can have any number of default parameters, and they can appear on (almost) any parameter. Rest parameters have some rules to follow—there must be at most one of them in the parameter list and it has to be the last one if present. What happens if we mix them together? For some reason this brings back memories of mixing barium with another chemical back in the high school chemistry lab—that was the last day they let me in there.

 const​ product = ​function​(first, second = 1, ...moreValues) {
  console.log(first + ​', '​ + second + ​', length:'​ + moreValues.length);
 };

The product() method names a required parameter, a default parameter, and a rest parameter. There are a few consequences of this design.

Recall that the default parameter may be omitted and the rest parameter may receive zero or more values. Since the rest parameter has to be the last, when present, there is no way to omit the default parameters if the function also uses a rest parameter. So, in short, the default value is pretty useless, unless the user wants to use undefined—but we know that results in pungent smelling code.

Having discussed default values and the rest parameter, it’s hard to avoid the nefarious thought of giving a default value to the rest parameter. Let’s give that a try:

 //BROKEN CODE
 const​ notAllowed = ​function​(first, second, ...moreValues = [1, 2, 3]) {}

If we do not provide any values for the rest parameter, then we want it to assume the values [1, 2, 3]. And JavaScript says:

 const notAllowed = function(first, second, ...moreValues = [1, 2, 3]) {}
  ^
 
 SyntaxError: Rest parameter may not have a default initializer

The good news here is we get an error at the point of declaration of the function, so it’s instant rejection and that’s good. When values are omitted for the rest parameter, it is required to be an empty array—no default values permitted there.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset