More Useful Function Patterns: Arbitrary Arguments

What works for one,
may work for many...
  
        - Eccason,
        Maester of the Aethis

An Arbitrary Number of Arguments

Sometimes you write a function that performs some sort of action based on a single argument. Some time later you realize that it would be nice if you could use that same function, but this time, with an arbitrary number of arguments. You may be tempted to change the interface of the original function to take an array instead of a single item. In doing so, however, you break the code that is using the function and force any future developers to wrap the arguments being sent inside an array, even when there’s only a single argument.

In this specific scenario, you may want to do something different. You may want to keep the function interface as it is and elegantly extend it to support multiple arguments without affecting any existing code. That’s what we do in C# with the params keyword, and what you can achieve in JavaScript via the arguments object or ES6 rest parameter syntax.

This is what you’ll learn in this chapter. But first, did you know that JavaScript functions are pretty peculiar about their arguments?

The Craziness Of Function Arguments in JavaScript

JavaScript gives you a lot of freedom when it comes to handling function parameters and arguments. For instance, you can declare a function with a specific number of parameters and, if you wish, call it without any arguments at all.

Imagine a function heal that casts a healing spell on a person:

1 function heal(person, inchantation, modifier){
2   var healedHp;
3   modifier = modifier || 1;
4   healedHp = modifier * 100;
5 
6   console.log('you heal ' + person + ' with ' + inchantation + 
7               ' spell (+' + healedHp + 'hp)' );
8   person.hp += healedHp;
9 }

This function has an arity of 3, that is, it expects 3 arguments. You can verify this accessing its length property. (A popular interview question by the way):

1 console.log(heal.length);
2 // => 3

Thanks to the magic of JavaScript’s infinite freedom you can call it without any arguments at all. Although this doesn’t mean that it will work:

1 try {
2   heal();
3 } catch (e) {
4   console.log(e)
5 }
6 // => you heal undefined with undefined spell (+100hp)
7 // => TypeError: cannot read property hp of undefined

You can also call it with as many of those 3 arguments as you want:

 1 var JonSnow = {name: 'Jon Snow', hp: 5, 
 2                toString: function(){return this.name;}};
 3 
 4 heal(JonSnow);
 5 // => you heal Jon Snow with undefined spell (+100hp)
 6 
 7 heal(JonSnow, 'cure');
 8 // => you heal Jon Snow with cure spell (+100hp)
 9 
10 heal(JonSnow, 'super heal', /* modifier */ 2);
11 // => you heal Jon Snow with super heal spell (+200hp)

Or even with more arguments than the parameters defined in the function itself:

1 heal(JonSnow, 'heal', 1, 3, 2, 3, 'cucumber', 
2      {a: 1, b:2}, 'many arguments');
3 // => you heal Jon Snow with heal spell (+100hp)

It is up to you to implement the body of a function and handle each case as you see fit. But what happens when you want to write a function that takes an arbitrary number of arguments?

Functions with Arbitrary Arguments Right Now!

In C#, whenever we want to write such a function we use the params keyword. In JavaScript (up to ES5), on the other hand, there’s no keyword nor operator that allows us to do that.

The only possible approach is to use the arguments object. arguments, like this, is a special kind of object present within every function in JavaScript. Its whole purpose is to give access to the arguments used to call a function from inside the function itself.

Think about an obliterate spell that you could use to completely wipe out an enemy:

1 function obliterate(enemy){
2   console.log(enemy + " wiped out of the face of the earth");
3 }
4 
5 obliterate('batman');
6 // => batman wiped out of the face of the earth

But why stop at one? With arguments you can make an obliterate spell to wipe out all your enemies at once!

 1 function obliterate(){
 2   // Unfortunately arguments is not an array :O
 3   // so we need to convert it ourselves
 4   var victims = Array.prototype.slice.call(arguments, /* start */ 0);
 5 
 6   victims.forEach(function(victim){
 7     console.log(victim + " wiped off of the face of the earth");
 8   });
 9   console.log('*Everything* has been obliterated, ' + 
10               'oh great master of evil and deceit!');
11 }

Indeed, we can use our obliterate function with one or as many arguments as we want. We have extended our function API without breaking it:

 1 obliterate('batman');
 2 // => batman wiped out of the face of the earth
 3 // *Everything* has been obliterated, oh great master 
 4 // of evil and deceit!
 5 
 6 obliterate("John Doe", getPuppy(), 1, new Minion('Willy', 'troll'));
 7 /*
 8 John Doe wiped off of the face of the earth
 9 cute puppy wiped off of the face of the earth
10 1 wiped off of the face of the earth
11 Willy the troll wiped off of the face of the earth
12 *Everything* has been obliterated, oh great master 
13 of evil and deceit!
14 */
15 
16 function getPuppy(){
17   return {
18     cuteWoof: function(){console.log('wiii');},
19     toString: function(){return 'cute puppy';}
20   };
21 };
22 
23 function Minion(name, type){
24   this.name = name;
25   this.type = type;
26   this.toString = function(){ return name + " the " + type;};
27 }

As it goes, there are a couple of interesting things to point out in this example.

The first one is the fact that we are sending arguments of different types to the function and they are all treated seamlessly. That’s an example of duck typing in action where an object is defined by what it can do and not by its type. As long as the values that we pass as arguments support the interface the function expects - in this case the toString method - everything will work just fine.

The second thing to point out is the use of the Array.prototype.slice function. We use it to convert the arguments variable into a real array victims. While you would expect the arguments variable to be an array, it is not, it is an array-like object (and this, my friend, is another of JavaScript quirks right here).

Array-Like Objects

Here are the things you can do with an array-like object:

 1 function inspectArguments(){
 2 
 3   // you can index it:
 4   console.log("the first argument is ", arguments[0]);
 5   console.log("the second argument is ", arguments[1]);
 6 
 7   // you an enumerate it:
 8   // as in the arguments are enumerable like
 9   // any common array or object, and thus you can use
10   // the for/in loop
11   for (var idx in arguments) {
12     console.log("item at position " + idx + 
13                 " is " + arguments[idx]);
14   }
15 
16   // it has a length property
17   console.log("there are " + arguments.length + " arguments");
18 }
19 
20 inspectArguments("cucumber", "dried meat", "dagger", "rock");
21 // => the first argument is cucumber
22 // => the second argument is dried meat
23 // => item at position 1 is cucumber
24 // => etc...
25 // => there are 4 arguments

Because it is not an array it does not have any of the array functions that you would expect:

 1 function inspectArgumentsFunctions(){
 2   console.log("arguments.foreach is ", arguments.foreach);
 3   console.log("arguments.map is ", arguments.map);
 4   console.log("arguments.some is", arguments.some);
 5 }
 6 
 7 inspectArgumentsFunctions();
 8 // => arguments.foreach is undefined
 9 // => arguments.map is undefined
10 // => arguments.some is undefined

Using the slice function of Array.prototype allows you to convert arguments to an array and take advantage of all the nice array functions. This is what we did with the obliterate function:

1 function obliterate(){
2   //...
3   var victims = Array.prototype.slice.call(arguments, /* start */ 0);
4   victims.forEach(function(victim){
5       console.log(victim + " wiped out of the face of the earth");
6   });
7   //...
8 }

Alternatively, you can use the ES6 Array.from method for a more natural conversion of an array-like object into an array:

1 function obliterate(){
2   //...
3   var victims = Array.from(arguments);
4   victims.forEach(function(victim){
5       console.log(victim + " wiped out of the face of the earth");
6   });
7   //...
8 }

In the specific case of the arguments object ES6 rest syntax offers a much better alternative9 as you’ll find out soon.

Native Arbitrary Arguments with ES6 Rest Parameters

ES6 comes with a native way to handle arbitrary arguments: rest parameters.

All the complexity of using the arguments object in the previous examples can be substitued by rest parameters and handled seamlessly:

1 function obliterate(...victims){
2   victims.forEach(function(victim){
3     console.log(victim + " wiped out of the face of the earth");
4   });
5   console.log('*Everything* has been obliterated, ' + 
6               'oh great master of evil and deceit!');
7 }

When using rest parameters, victims becomes an array automatically so there’s no need to perform additional conversions. Indeed, if we use the new obliterate function as we did before it works perfectly:

 1 obliterate('batman');
 2 // => batman wiped out of the face of the earth
 3 // *Everything* has been obliterated, oh great master 
 4 // of evil and deceit!
 5 
 6 obliterate("John Doe", getPuppy(), 1, new Minion('Willy', 'troll'));
 7 /*
 8 John Doe wiped off of the face of the earth
 9 cute puppy wiped off of the face of the earth
10 1 wiped off of the face of the earth
11 Willy the troll wiped off of the face of the earth
12 *Everything* has been obliterated, oh great master of evil and decei\
13 t!
14 */

Rest paratemers can also be used in combination with normal parameters. For instance, imagine that the obliterate spell had an extra effect on the first enemy it encountered. We could rewrite it like this:

 1 function obliterate(unfortunateVictim, ...victims){
 2   console.log(unfortunateVictim + 
 3              " wiped out of the face of EXISTENCE " +
 4              "as if it had never existed.... Woooo");
 5   victims.forEach(function(victim){
 6     console.log(victim + " wiped out of the face of the earth");
 7   });
 8   console.log('*Everything* has been obliterated, ' + 
 9               'oh great master of evil and deceit!');
10 }

Upon using this malignant spell the first enemy would be removed from existence completely and utterly:

1 obliterate("John Doe", getPuppy(), 1, new Minion('Willy', 'troll'));
2 /*
3 John Doe wiped out of the face of EXISTENCE as if it had 
4 never existed.... Woooo
5 cute puppy wiped off of the face of the earth
6 etc...
7 */

Note how easily the rest parameters capture all the remaining arguments after unfortunateVictim. Beautiful!

You now know different ways to implement a function that takes an arbitrary number of arguments. A function that provides the same API regardless of being called with one or many arguments. But what if you just happen to have an array? What happens if the output of another function is an array of enemies that need to be obliterated? Well, that’s when the ES6 spread operator comes in handy.

The ES6 spread operator, among other things that you’ll learn later in this book, lets you seamlessly call these type of functions using an array as argument:

 1 let mortalEnemies = ["John Doe", getPuppy(), 1, 
 2                      new Minion('Willy', 'troll')];
 3 
 4 obliterate(...mortalEnemies);
 5 /*
 6 John Doe wiped out of the face of EXISTENCE as if 
 7 it had never existed.... Woooo
 8 cute puppy wiped off of the face of the earth
 9 etc...
10 */

Note how the spread operator ...mortalEnemies is super similar to rest parameters syntax but performs the opposite operation, instead of gathering arguments into an array, it spreads an array into arguments.

Concluding

Congratulations! You have cleared another obstacle in the path to writing functions with beautiful and thoughtful APIs.

Function parameters and arguments are yet another example of the flexibility and freedom that JavaScript offers. In JavaScript you can call a function with as many argument as you want regardless of the signature of the function itself.

In some ocassions you’ll need to design a function that takes an arbitrary number of arguments. If you are using ES5, you can take advantage of the arguments object. The arguments object is present in every function and gives you access to the arguments with which a function was called. Unfortunately, it is an array-like object and you may need to convert it to an array before you can use it. If you are using ES6 or above, rest parameters offer a great developer experience similar to C# params. You can combine rest parameters with normal parameters and even call one of these special functions with an array using the spread operator.

In the next chapter you’ll find out how to override functions (and methods) in JavaScript.

Exercises