Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

JavaScript Function Arguments

Chapter 32 45 mins

Learning outcomes:

  1. Arity of a function and the length property
  2. The arguments object
  3. The arguments.callee property
  4. Rest parameters
  5. The spread operator
  6. Default-valued parameters

Introduction

As we learnt in the previous chapter, function are an extremely useful feature of programming. And to functions, what's a lot useful is the idea of parameters and arguments.

JavaScript provides quite a decent amount of utilities and notations to work with parameters and arguments in various ways. Some are quite old and supported to date, while some are pretty recent and elegant in accomplishing the same things the old utilities are capable of.

In particular, we'll cover the arguments object in detail, and reference about its internal implementation from the ECMAScript spec; consider default-valued parameters, rest parameters and much more.

Let's start exploring...

The arity of a function

In mathematics and programming, a function's arity refers to the number of parameters defined in function.

In the case of functions in JavaScript, arity refers to the number of parameters given in the function's definition, excluding default-valued, rest and destructured parameters.

We'll cover all these three classifications of parameters later on in this very chapter.

In the code below, we define a function sum() with two parameters a and b:

function sum(a, b) {
   return a + b;
}

What do you think would be the arity of sum()? Well, there are two parameters, likewise the arity is 2.

Similarly, below we define a function with three parameters b, n and m, and hence an arity of 3:

function modExp(b, n, m) {
   return (b ** n) % m;
}

Here, modExp() represents the modular exponentiation operation used in computer science, particularly in cryptography.

Now in both the code snippets above, we are determining the arity of the given function ourself by reading the source code. It would've been much better if we could programmatically retrieve this value.

Well, it turns out that there is a way and it's very simple!

To inspect the arity of given function in JavaScript, we access its length property.

As stated before, the length property would only consider non-rest, non-default-valued, and non-destructed parameters of a given function.

Let's try accessing length for both the functions given above.

First for the sum() function:

function sum(a, b) {
   return a + b;
}

console.log('Arity:', sum.length);
Arity: 2

Now, for the modExp() function:

function modExp(b, n, m) {
   return (b ** n) % m;
}

console.log(modExp.length);
Arity: 3

Amazing. Just as we expected.

In practical JavaScript code, we might not need to know the arity of given functions very often. But if we ever do need to, we'd know what property to use.

The arguments object

Often times, we're more interested in figuring out the number of arguments actually passed into a function when invoking it. This couldn't always reliably be done by the function's length property.

For instance, consider the definition of sum() below:

function sum(a, b) {
   console.log('Arity within the function:', sum.length);
   return a + b;
}

sum(10, 20, 30);

console.log('Arity outside the function:', sum.length);
Arity within the function: 2
Arity outside the function: 2

The function clearly has an arity of 2, likewise sum.length always gives 2, whether we access the property outside the function, or within the function. But notice that when we invoke sum() in line 6, we actually pass in 3 arguments.

JavaScript provides an array-like object to all functions, excluding arrow functions, which could be used to inspect the total number of arguments passed to them, regardless of the fact that those arguments actually align with given parameters.

That is the arguments object.

arguments is a local variable created every time a function is called. It points to an array-like object that holds the list of all arguments passed into the function.

It looks and works just like an array i.e. has indexed elements starting at the index 0 and a length property to denote the total number of elements, and is therefore termed as 'array-like'.

However, keep in mind that arguments is not actually an array. We couldn't call array methods such as slice(), sort(), push() etc. on it.

Let's see what arguments looks like:

First, we'll inspect the arguments object as it is and see its structure:

function f() {
   console.log(arguments);
}

f(10);
f(10, 'Hello World!');
f(10, 20, 'Hello World!');
f(true, false, 50, 40, 30);
Arguments [10, ...]
Arguments [10, 'Hello World!', ...]
Arguments [10, 20, 'Hello World!', ...]
Arguments [true, false, 50, 40, 30, ...]
The ... in the code above represents other properties on the arguments object. Try executing the code above in the console window, and see the output.

Let's see arguments in some real action.

Consider the extended definition of sum() below:

function sum(a, b) {
   if (arguments.length === 3) return a + b + arguments[2];
   return a + b;
}

console.log(sum(10, 20));
console.log(sum(10, 20, 50));
console.log(sum(10, 20, 50, 60));
30
80
30

When the function is called, we check the number of args passed to it. If 3 args are given, we add them all together and return the sum. Note that the third argument doesn't have a corresponding parameter, likewise to access it we have to use arguments[2].

However, if the number of args is not equal to 3, we just return the sum of the first two arguments.

This definition of sum() is definitely a bit more flexible than the last definition that was capable of adding only two args. But it's still not that flexible. We could pass in 4, or more, args whereby only the first two would be considered.

A much more robust definition of sum() would require iterating over all elements in arguments and adding them together. There are multiple ways of performing this iteration — using the for loop, or array methods such as forEach() and reduce() with a clever trick.

We'll see this trick in the chapter JavaScript Function Methods.

One important thing to note in the code above is that we refer to the first 2 args of sum() by the names a and b respectively; not as arguments[0] and arguments[1]. The latter is also possible.

An illustration is shown below:

function sum(a, b) {
   if (arguments.length === 3) {
      return arguments[0] + arguments[1] + arguments[2];
   }
   return arguments[0] + arguments[1];
}

console.log(sum(10, 20));
console.log(sum(10, 20, 50));
console.log(sum(10, 20, 50, 60));
30
80
30

This definition is exactly the same as before, except for that now instead of a and b, we use arguments[0] and arguments[1], respectively.

Exotic arguments object

According to the ECMAScript spec, when a function created using the function keyword is in non-strict mode, and doesn't have any modern ES6-style parameters, the arguments object created for it is called an exotic arguments object.

It works a bit differently than an ordinary arguments object. We'll get to the latter very shortly; first let's see what exactly is so special about an exotic arguments object.

When a function is invoked, all the parameters, that have been provided with arguments, are linked with corresponding entries in arguments, if the object is an exotic arguments object.

This merely means that if we change one, the other is changed automatically.

Didn't understand a word? Well, let's consider an example to help understand the definition above.

Below we have a simple function f() which is called on the next line after being declared:

function f(a) {
   a = 'new';

   console.log(a);
   console.log('arguments[0]:', arguments[0]);
}

f('old');

Inside the function, in line 2, we change the value of the parameter a and then inspect both a and arguments[0]. a would obviously change, but the most interesting thing is that arguments[0] changes as well.

What happens internally is as follows:

When the function f() is invoked, the arguments object is created containing all the arguments passed in f('old') — in this case, the arg 'old'. Then, each item in this list is linked with the corresponding parameter in the function's definition.

In this case, this simply means that the parameter a and the entry arguments[0] are both linked with one another. If we change one, the other is changed automatically.

Let's try to go the other way round — change the entry arguments[0] and then see whether the parameter gets updated:

function f(a) {
   arguments[0] = 'new';

   console.log('arguments[0]:', arguments[0]);
   console.log('a:', a);
}

f('old');
arguments[0]: new
a: new

Yup, this works as well!

This confirms the fact that both the values are linked with one another — updating one automatically updates the other. We say that both the values are in sync with one another.

Note that, as stated before, each argument in arguments is linked with its corresponding parameter. This means that if a function, that requires an argument, is called without one, no linking would be made between the parameter and the corresponding entry in arguments.

This is simply because there is no entry in arguments for which linked could be done.

Consider the code below:

function f(a, b) {
   b = 'new';
   console.log('b', b);
   console.log('arguments[1]:', arguments[1]);
}

f('old');

Here, f() has two parameters a and b, but when we call it in line 7, we just pass in one argument, which obviously goes into the parameter a. The parameter b is simply omitted.

The arguments object of f() has one entry in it, and that's linked with a. From this point onwards, there is no entry in arguments likewise no link exists for b.

Hence, even if we assign a value to b inside the function f(), the corresponding position in arguments would remain undefined (in fact, it won't even exist).

Linkages are only created one time, and that is while iterating over the arguments object. After that, no linkages can be made.

If we delete an element in arguments using the delete keyword for which a link exists, that link is severed in the deletion process. Now changing the respective parameter or creating the entry back in arguments won't change the other.

This can be seen in the code below:

Keep in mind, that the entire linkage behavior described here holds only for an exotic arguments object. This is the exotic feature it has to offer — parameters and entries in the object are connected to one another.

And restating it, exotic arguments objects are only created if the function is non-strict and doesn't use modern ES6-style parameters, such as rest, default-valued parameters etc. If these prerequisites aren't met, the argument object is termed as being 'ordinary'. Details follow in the next bit.

Read more about exotic arguments objects in the specification itself at https://262.ecma-international.org/12.0/#sec-arguments-exotic-objects.

Ordinary arguments object

Exotic arguments objects are — after all — quite exotic and flexible. They store values that are linked with corresponding parameters (if the parameters exist).

Now, if you think for a second, how useful is this feature? Or simply put, how often would you need this kind of linkage in your applications?

You change a parameter's value some other value. This would change arguments[0] as well. But, if you'd be using the parameter in the function directly and not arguments[0] (which would usually be the case), wouldn't this update to arguments be redundant?

Managing links to automatically update a parameter or an entry in arguments carries a bit of overhead with it. Some amount of time is definitely wasted in performing this exotic behavior.

Strict mode in JavaScript was created to enforce better programming practices and meaningful internal behavior. Thus, the idea of exotic arguments objects becomes useless when we have a strict function.

A function running in strict mode, or using modern ES6-style parameters, creates an arguments object referred to, by the ECMAScript spec, as an ordinary arguments object.

Let's see what's so ordinary about it.

With an ordinary arguments object, there is simply no concept of linkage between parameters and corresponding entries in arguments. Changing one won't change the other.

Yeah, well this looks like some ordinary behavior. But sometimes, actually most of the times, being ordinary is better than being exotic. Isn't it so?

Apart from this difference, it's also invalid to access the callee property of arguments when it is ordinary. We'll see more information regarding callee in the next section

The arguments.callee property

Since the very first spec of ECMAScript, the arguments object of a function has a property callee on it which points to the function itself.

The purpose of arguments.callee was to be able to call an anonymous function recursively. Apart from it, there was no other way to do so, unless the anonymous function was assigned to some identifier.

Consider the code below:

var nums = [1, 2, 3, 4, 5, 6, 7];

var fibs = nums.map(function(n) {
   if (n === 1) return 0;
   if (n === 2) return 1;
   return arguments.callee(n - 1) + arguments.callee(n - 2);
});

console.log(fibs);
[0, 1, 1, 2, 3, 5, 8]

Here we define an anonymous function as an argument to map() to calculate the Nth term in a Fibonacci sequence. The function calls itself recursively using arguments.callee.

Now, arguments.callee was useful only before the advent of named function expressions, when it was the sole way to invoke anonymous functions recursively. Even now, it may be useful in some very rare scenario, but largely it's recommended to avoid it, and use named function expressions instead.

arguments.callee reduces the readability of code, and is relatively slower as compared to calling a function via its name. Plus, with it it's not mandatory to name the function — naming functions is highly useful if we want to be able to debug errors easily, as names show in stack traces.

Note that it's only an exotic arguments object that allows the callee property to be accessed. If the object is not exotic, i.e. it's an ordinary arguments object, then accessing its callee property throws an error.

But why?

Recall that an arguments object is ordinary when it is in strict mode (or when it uses modern ES6-style parameters, but this is not useful for the moment), and as we know, strict mode aims to rectify many bad coding pratices in JavaScript which are otherwise allowed in sloppy mode (a fancy name for non-strict mode).

arguments.callee is one of these bad coding practices. It's long, kind of ugly in code, and slower than accessing the respective function via its name. Hence, it is disallowed in strict mode.

MDN has a nice section further detailing about strict mode and arguments.callee. You can read it at Strict mode - JavaScript | MDN — Making eval and arguments simpler.
It's also invalid to assign a value to arguments.callee in the case of an exotic arguments object.

Rest parameters

ECMAScript 2015, also known as ES6, introduced tons and tons of elegant syntaxes and utilities intto the modern language to perform common operations more easily. One of them is rest parameters.

A rest parameter encapsulates all the remaining arguments after the arguments for all non-rest parameters in the form of an array.

It's denoted by preceding the name of the parameter with ellipsis (...). So if the name for the rest parameter is param, it would be denoted as ...param.

There can be only one rest parameter in a function, and it ought to be the last parameter. With these two prerequisites met, the parameter works as follows:

When a function is invoked that has a rest parameter in it, each of the parameters preceding the rest parameter is resolved down with its respective argument. That is, the first argument goes into the first parameter, the second one goes into the second parameter and so on.

When all the preceding parameters are resolved, the rest of the arguments are encapsulated in a fresh array and this assigned to the rest parameter.

Did you notice the use of the word 'rest' above? That is where the name 'rest parameter' comes from — it contains the rest of the arguments.

Let's consider an example:

Say we have a function showStudentInfo() that takes in the name of a student and then a list of marks obtained by him/her in recent exams, in the form of mutiple arguments.

The list of marks can be variable as each student takes a variable number of subjects. Hence, to encapsulate all these arguments, the function makes the second parameter a rest parameter.

Here's the code of the function:

function showStudentInfo(name, ...marksList) {
   console.log('Student name:', name);
   console.log('Marks obtained:', marksList.join(', '));
   console.log('-----'); // a divider line
}

showStudentInfo('Alice', 67, 80, 80, 91);
showStudentInfo('Bob', 85, 90, 95);
Student name: Alice
Marks obtained: 67, 80, 80, 91
-----
Student name: Bob
Marks obtained: 85, 90, 95
-----

Note that the function above could've been implemented using arguments as well, but not without processing the object first i.e converting it into an array, then slicing it from index 1 upto its end, and then calling the join() method on this slice. Too much work, isn't it?

With a rest parameter, we don't have to worry about any of these since all the arguments denoting the marks of the students are nicely laid out in an actual array.

Anyways, let's consider another example:

Below we have a function showLangInfo() that displays information about a programming language:

function showLangInfo(name, yearReleased, ...influencingLangs) {
   console.log(name + ' was first released in ' + yearReleased + '.');
   console.log('It was influenced by:');
   console.log(' - ' + influencingLangs.join('\n - '));
}

The first parameter holds the name of the programming language, the second holds the year in which it was first released, and the last one, which is a rest parameter, holds a list of some languages that were influenced by this language.

Let's call the function:

function showLangInfo(name, yearReleased, ...influencingLangs) {
   console.log(name + ' was first released in ' + yearReleased + '.');
   console.log('It was influenced by:');
   console.log(' - ' + influencingLangs.join('\n - '));
}

showLangInfo('JavaScript', 1995, 'Python', 'Java', 'Scheme');
JavaScript was first released in 1995.
It was influenced by:
- Python
- Java
- Scheme

If the languages influencing a programming language are not known, showLangInfo() can be called with just two args — influencingLangs would resolve to an empty array in this case.

showLangInfo('ABC', 1987);
ABC was first released in 1987.
It was influenced by:
-

Moving on, in both the examples above, there was at least one parameter preceding the rest parameter. This is not required at all. A function could have only one parameter and that being a rest parameter.

Let's create a function sum() that takes in an arbitrary number of arguments and adds them all together. The addition is accomplished using a for loops:

Once again, note that this function could've been made to use arguments instead of a rest parameter. And in this case, of summing all the args, arguments would have performed quite the same way.

No support on old browsers!

While using rest parameters in your code, keep in mind that they are part of ES6, and hence aren't supported on old browsers such as IE8 and before.

If you want your program to work consistently on these browsers, consider shifting to arguments or some other way to accomplish things in your program. If you don't want to change the code, another possible option is to use a transpiling tool that converts modern JavaScript into old JavaScript that's well supported on all old browsers.

Some popular transpiling choices are BabelJS and Traceur.

As stated before, specifying more than one rest parameter inside a function, or specifying the rest parameter before some other parameter leads to an error:

The following code snippets illustrates this:

// invalid to have more than one rest param
function f(...rest1, ...rest2) {
   console.log(rest1, rest2);
}
// the rest param must be the last param
function f(...rest1, a) {
   console.log(rest1, a);
}
Uncaught SyntaxError: Rest parameter must be last formal parameter

Both the snippets above log the same error, even though in the first one, the rest parameter is indeed the last parameter.

Well, if we see it carefully, the first snippet has a rest parameter before the last parameter. This will always be the case, when more than one rest parameter is specified — the first one would always be before some other parameter.

Hence, the JavaScript engine simplifies its work and just checks for when the rest parameter is not the last one in the list of parameters and logs an error if it isn't.

Compiler designers sure have to think cleverly.

The spread operator

ES6 provides an operator that works opposite to how the rest param works. It's called the spread operator.

It's idea is pretty apparent in its name. Let's see it...

The spread operator converts an iterable sequence into a list of arguments.

It's denoted as ...iterable, where iterable is the iterable sequence to spread into a list of arguments.

...iterable

OK, but what exactly is an 'iterable sequence'?

An iterable sequence simply refers to any object that can be iterated over. Examples of iterable sequences that we've seen so far in this course are strings, arrays, and arguments.

As we shall see in the subsequent segment of this course, there are numerous iterable sequences frequently used in JavaScript such as HTMLCollection, NodeList, DOMTokenList, HTMLOptionsCollection and so on.

The spread operator can convert any of these into a list of arguments. Let's consider the operator as working on arrays.

Recall from the chapter JavaScript Math object, the method Math.min() takes an arbitrary number of args and returns the minimum value amongst them.

Now, more often than not, it's the case that the numbers are available as part of an array. The problem is that passing an array is useless to Math.min() — it finds the min value in the list of arguments, not in any one argument.

Consider the following:

var nums = [1, 50, 9, -4, -50, 0, 0, 10, 15, 13];
var min = Math.min(nums);

console.log('The minimum number is:', min);
The minimum number is: NaN

The minimum value output here is NaN although there is no such value in the array. This is because the Math.min() method never goes inside the array — it converts each of its arguments into a number and then throws out the minimum value amongst them.

And we already know from the JavaScript Arrays — Basics chapter that an array with more than one element always gets converted into NaN.

So, this means that Math.min() can't operate on an array.

Any other solutions for this problem of finding the minimum number in an array?

Well, you guessed it. We convert the array into a list of arguments using the spread operator.

Shown below is an illustration:

var nums = [1, 50, 9, -4, -50, 0, 0, 10, 15, 13];
var min = Math.min(...nums);

console.log('The minimum number is:', min);
The minimum number is: -50

All the magic lies in the expression ...nums. What it does is simply converts the array into a list of arguments, which the Math.min() method could then easily process.

Easy?

Default-valued parameters

Prior to ES6, there was no concept of default-valued parameters in JavaScript. While calling a function, if an argument was omitted, its corresponding parameter would simply be set to undefined.

Consequently, developers usually had to setup some boilerplate code to use a default value in this case, as shown below:

function f(a, b) {
   b = (b === undefined) ? 'default' : b;
   console.log(a, b);
}

// second argument provided
f(100, 200);

// second argument omitted
f(100);
100 200
100 default

This works. But after ES6, there's a much simpler syntax to achieve this. It's called a default-valued parameter.

It's denoted as follows:

function function_name(param1, param2 = defaultValue, param3, ...) {
   // code
}

The parameter is denoted by putting an assignment syntax, i.e. an equals sign (=) followed by a value, after the parameter's name in the function definition.

Note that the defaultValue expression is computed each time the function is invoked.

Python also supports the idea of default-valued parameters, however the default values are computed only once when compiling the function.

This means that we could dynamically compute the default value using a function. We'll show such an example below.

Let's consider an extremely simple example.

Following we define a greeting function which takes in an argument, concatenates it with the text 'Hello' and finally outputs the result. When the function is called without an argument, the parameter is defaulted to 'World!'.

function greeting(txt = 'World!') {
   console.log('Hello ' + txt);
}

greeting('JavaScript');
greeting();
Hello JavaScript
Hello World!

Not really difficult, was it?

Let's consider one more example.

Below we define a function createMatrix() that takes in the dimensions of a matrix and returns the respective 2D array back with all elements initialised to 0:

function createMatrix(rows, cols = rows) {
   return new Array(rows).fill(0).map(function(e) {
      return new Array(cols).fill(0);
   });
}

console.log(createMatrix(3));

The number of rows are provided in the first row argument while the number of columns are provided in the secone one. In case if the number of columns are omitted, a square matrix is created i.e. with the same number of rows and columns.

Note that in the code above, the parameter cols has access to the value of the parameter rows, defined before it. We'll see how is this possible internally in the section below.

In pratical code, this createMatrix() function could be useful.

Why not consider another example?

In the code below, we create a generic function f() that takes in three arguments and outputs them back as is. If the third argument is not provided, it's taken to be the value of the first argument multiplied by 5:

function f(a, b, c = a * 5) {
   console.log(a, b, c);
}

f(100, 200, 300);
f(100, 200);
100 200 300
100 200 500

Alright, as stated before, it's time to see how is a default-valued parameter able to access any of the preceding parameters.

Default-valued parameters accessing preceding parameters

Recall that parameters are simply local variables created when a function is called.

At a function's invocation, the whole list of arguments passed in is consolidated into an arguments object, then it's determined whether or not this object would be exotic or ordinary. In the case of a function having a default-valued parameter, arguments is ordinary.

Next, each parameter is created as a local variable, and assigned the corresponding argument, exactly in the order it appears in the function's header. For instance, in the case above, first a variable a is created and initialised to the argument 10, then a variable b is created and initialised to the argument 20.

When assigning to a parameter, if it's a default-valued parameter, then its argument is checked for being undefined. If it is undefined, the default value of the parameter is instead computed and assigned to the parameter.

At this point, all the preceding parameters are already created as local variables in memory with the corresponding arguments stored inside them. Likewise, the default-valued parameter can refer to them easily.

Internally, the function f() above could be thought of as working like the following:

function f() {
   var a = arguments[0],
       b = arguments[1],
       c = (arguments[2] === undefined) ? a * 5 : arguments[2];

   console.log(a, b, c);
}

f(100, 200, 300);
f(100, 200);
100 200 300
100 200 500

This roughly depicts how are parameters implemented internally in JavaScript, although the internals don't obviously rely on these statements. In fact, the internals are what define these statements as well!