JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

20. Callable values

20.1. Kinds of functions

JavaScript has two categories of functions:

Let’s look at ordinary functions and specialized functions in greater detail.

20.1.1. Ordinary functions

Ordinary functions are created via:

// Function declaration (a statement)
function ordinary1(a, b, c) {
  // ···
}

// Anonymous function expression
const ordinary2 = function (a, b, c) {
  // ···
};

// Named function expression
const ordinary3 = function myName(a, b, c) {
  // `myName` is only accessible in here
};

Let’s examine the parts of a function declaration via an example:

function add(x, y) {
  return x + y;
}
20.1.1.1. Roles played by ordinary functions

Consider the following function declaration from the previous section:

function add(x, y) {
  return x + y;
}

This function declaration creates an ordinary function whose name is add. As an ordinary function, add() can play three roles:

20.1.2. Specialized functions

Specialized functions are specialized versions of ordinary functions. Each one of them only plays a single role:

Apart from nicer syntax, each kind of specialized function also supports new features, making them better at their job than ordinary functions.

Arrow functions are explained later in this chapter. Methods are explained in the chapter on single objects. Classes are explained in the chapter on prototype chains and classes.

Tbl. 16 lists the capabilities of ordinary and specialized functions.

Table 16: Capabilities of the four kinds of functions.
Ordinary function Arrow function Method Class
Function call
Method call lexical this
Constructor call

It’s important to note that arrow functions, methods and classes are still categorized as functions:

> (() => {}) instanceof Function
true
> ({ method() {} }.method) instanceof Function
true
> (class SomeClass {}) instanceof Function
true

20.1.3. More kinds of real functions and methods

Warning: You are about to be faced with a long list of things and few explanations. This is just to give you a brief overview. This chapter focuses on synchronous real functions (all of which we have already seen). Later chapters will explain everything we are about to see.

So far, we have always written simple, synchronous code. Upcoming chapters will cover two more modes of programming in JavaScript:

Two variations of real functions and methods help with these modes of programming:

Both variations can be combined, leading to a total of eight different constructs (tbl. 17).

Table 17: Different kinds of real functions and methods.
sync vs. async generator vs. not real function vs. method
sync function
sync method
async function
async method
sync generator function
sync generator method
async generator function
async generator method

Tbl. 18 gives an overview of the syntax for creating these constructs (a Promise is a mechanism for delivering asynchronous results).

Table 18: Syntax for creating real functions and methods.
Sync function Sync method Result Values
function f() {} { m() {} } value 1
f = function () {}
f = () => {}
Sync generator function Sync gen. method
function* f() {} { * m() {} } iterable 0+
f = function* () {}
Async function Async method Result Values
async function f() {} { async m() {} } Promise 1
f = async function () {}
f = async () => {}
Async generator function Async gen. method
async function* f() {} { async * m() {} } async iterable 0+
f = async function* () {}

For the remainder of this chapter, we’ll explore real functions and their foundations.

20.2. Named function expressions

Function declarations (statements) always have names. With function expressions, you can choose whether to provide a name or not.

The following is an example of a function expression without a name – an anonymous function expression. Its result is assigned to the variable f1:

const f1 = function () {};

Next, we see a named function expression (assigned to f2):

const f2 = function myName() {};

As mentioned before, named function expressions look exactly like function declarations, but they are expressions and thus used in different contexts.

Named function expressions have two benefits.

First, their names show up in stack traces:

const func = function problem() { throw new Error() };
setTimeout(func, 0);
// Error
//   at Timeout.problem [as _onTimeout] (repl:1:37)
//   at ontimeout (timers.js:488:11)
//   at tryOnTimeout (timers.js:323:5)
//   at Timer.listOnTimeout (timers.js:283:5)

You can see the name problem in the first line of the stack trace.

Second, the name of a named function expression provides a convenient way for the function to refer to itself. For example:

const fac = function me(n) {
  if (n <= 1) return 1;
  return n * me(n-1);
};
// `me` only exists inside the function:
assert.throws(() => me, ReferenceError);

assert.equal(fac(3), 6);

You are free to assign the value of fac to another value. It will continue to work, because it refers to itself via its internal name, not via fac.

20.3. Arrow functions

Arrow functions were added to JavaScript for two reasons:

  1. To provide a more concise way for creating functions.
  2. To make working with real functions easier: You can’t refer to the this of the surrounding scope inside an ordinary function (details soon).

20.3.1. The syntax of arrow functions

Let’s review the syntax of an anonymous function expression:

const f = function (x, y, z) { return 123 };

The (roughly) equivalent arrow function looks as follows. Arrow functions are expressions.

const f = (x, y, z) => { return 123 };

Here, the body of the arrow function is a block. But it can also be an expression. The following arrow function works exactly like the previous one.

const f = (x, y, z) => 123;

If an arrow function has only a single parameter and that parameter is an identifier (not a destructuring pattern) then you can omit the parentheses around the parameter:

const id = x => x;

That is convenient when passing arrow functions as parameters to other functions or methods:

> [1,2,3].map(x => x+1)
[ 2, 3, 4 ]

This last example demonstrates the first benefit of arrow functions – conciseness. In contrast, this is the same method call, but with a function expression:

[1,2,3].map(function (x) { return x+1 });

20.3.2. Arrow functions: lexical this

Ordinary functions can be both methods and real functions. Alas, the two roles are in conflict:

The following code demonstrates a common work-around:

const prefixer = {
  prefix: '==> ',
  prefixStringArray(stringArray) {
    const that = this; // (A)
    return stringArray.map(
      function (x) {
        return that.prefix + x; // (B)
      });
  },
};
assert.deepEqual(
  prefixer.prefixStringArray(['a', 'b']),
  ['==> a', '==> b']);

In line B, we want to access the this of .prefixStringArray(). But we can’t, since the surrounding ordinary function has its own this that shadows (blocks access to) the this of the method. Therefore, we save the method’s this in the extra variable that (line A) and use that variable in line B.

An arrow function doesn’t have this as an implicit parameter, it picks up its value from the surroundings. That is, this behaves just like any other variable.

const prefixer = {
  prefix: '==> ',
  prefixStringArray(stringArray) {
    return stringArray.map(
      x => this.prefix + x);
  },
};

To summarize:

20.3.3. Syntax pitfall: returning an object literal from an arrow function

If you want the expression body of an arrow function to be an object literal, you must put the literal in parentheses:

const func1 = () => ({a: 1});
assert.deepEqual(func1(), { a: 1 });

If you don’t, JavaScript thinks, the arrow function has a block body (that doesn’t return anything):

const func2 = () => {a: 1};
assert.deepEqual(func2(), undefined);

{a: 1} is interpreted as a block with the label a: and the expression statement 1.

20.4. Hoisting

Function declarations are hoisted (internally moved to the top):

assert.equal(foo(), 123); // OK

function foo() { return 123; }

Hoisting lets you call a function before it is declared.

Variable declarations are not hoisted – you can only use their variables after they were declared:

assert.throws( // before
  () => foo(),
  ReferenceError);

const foo = function () { return 123; };

assert.equal(foo(), 123); // after

Class declarations are not hoisted, either:

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.ok(new MyClass() instanceof MyClass);

20.5. Returning values from functions

You use the return operator to return values from a function:

function func() {
  return 123;
}
assert.equal(func(), 123);

Another example:

function boolToYesNo(bool) {
  if (bool) {
    return 'Yes';
  } else {
    return 'No';
  }
}
assert.equal(boolToYesNo(true), 'Yes');
assert.equal(boolToYesNo(false), 'No');

If, at the end of a function, you haven’t returned anything explicitly, JavaScript returns undefined for you:

function noReturn() {
  // No explicit return
}
assert.equal(noReturn(), undefined);

20.6. Parameter handling

20.6.1. Terminology: parameters vs. arguments

The term parameter and the term argument basically mean the same thing. If you want to, you can make the following distinction:

20.6.2. Too many or not enough arguments

JavaScript does not complain if a function call provides a different number of arguments than expected by the function definition:

For example:

function foo(x, y) {
  return [x, y];
}

// Too many arguments:
assert.deepEqual(foo('a', 'b', 'c'), ['a', 'b']);

// The expected number of arguments:
assert.deepEqual(foo('a', 'b'), ['a', 'b']);

// Not enough arguments:
assert.deepEqual(foo('a'), ['a', undefined]);

20.6.3. Parameter default values

Parameter default values specify the value to use if a parameter has not been provided. For example:

function f(x, y=0) {
  return [x, y];
}

assert.deepEqual(f(1), [1, 0]);
assert.deepEqual(f(), [undefined, 0]);

undefined also triggers the default value:

assert.deepEqual(
  f(undefined, undefined),
  [undefined, 0]);

20.6.4. Rest parameters

A rest parameter is declared by prefixing an identifier with three dots (...). During a function or method call, it receives an Array with all remaining arguments. If there are no extra arguments at the end, it is an empty Array. For example:

function f(x, ...y) {
  return [x, y];
}
assert.deepEqual(
  f('a', 'b', 'c'),
  ['a', ['b', 'c']]);
assert.deepEqual(
  f(),
  [undefined, []]);
20.6.4.1. Enforcing an arity via a rest parameter

You can use rest parameters to enforce arities. Take, for example, the following function.

function bar(a, b) {
  // ···
}

This is how we force callers to always provide two arguments:

function bar(...args) {
  if (args.length !== 2) {
    throw new Error('Please provide exactly 2 arguments!');
  }
  const [a, b] = args;
  // ···
}

20.6.5. Named parameters

When someone calls a function, the arguments provided by the caller are assigned to the parameters received by the callee. Two common ways of performing the mapping are:

  1. Positional parameters: An argument is assigned to a parameter if they have the same position. A function call with only positional arguments looks as follows.

  2. Named parameters: An argument is assigned to a parameter if they have the same name. JavaScript doesn’t have named parameters, but you can simulate them. For example, this is a function call with only (simulated) named arguments:

Named parameters have several benefits:

20.6.6. Simulating named parameters

JavaScript doesn’t have real named parameters. The official way of simulating them is via object literals:

function selectEntries({start=0, end=-1, step=1}) {
  return {start, end, step};
}

This function uses destructuring to access the properties of its single parameter. The pattern it uses is an abbreviation for the following pattern:

{start: start=0, end: end=-1, step: step=1}

This destructuring pattern works for empty object literals:

> selectEntries({})
{ start: 0, end: -1, step: 1 }

But it does not work if you call the function without any parameters:

> selectEntries()
TypeError: Cannot destructure property `start` of 'undefined' or 'null'.

You can fix this by providing a default value for the whole pattern. This default value works the same as default values for simpler parameter definitions: If the parameter is missing, the default is used.

function selectEntries({start=0, end=-1, step=1} = {}) {
  return {start, end, step};
}
assert.deepEqual(
  selectEntries(),
  { start: 0, end: -1, step: 1 });

20.6.7. Spreading (...) into function calls

The prefix (...) of a spread argument is the same as the prefix of a rest parameter. The former is used when calling functions or methods. Its operand must be an iterable object. The iterated values are turned into positional arguments. For example:

function func(x, y) {
  console.log(x);
  console.log(y);
}
const someIterable = ['a', 'b'];
func(...someIterable);

// Output:
// 'a'
// 'b'

Therefore, spread arguments and rest parameters serve opposite purposes:

20.6.7.1. Example: spreading into Math.max()

Math.max() returns the largest one of its zero or more arguments. Alas, it can’t be used for Arrays, but spreading gives us a way out:

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
> Math.max(-1, ...[-5, 11], 3)
11
20.6.7.2. Example: spreading into Array.prototype.push()

Similarly, the Array method .push() destructively adds its zero or more parameters to the end of its receiver. JavaScript has no method for destructively appending an Array to another one, but once again we are saved by spreading:

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);

  Exercises: Parameter handling

20.7. Understanding JavaScript’s callable values (advanced)

In order to better understand JavaScript’s many callable values, it is helpful to distinguish between:

The following two concepts help with categorizing callable values:

Let’s look at a few examples:

  Quiz

See quiz app.