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

23. Callable values



23.1. Kinds of functions

JavaScript has two categories of functions:

The next sections explain what all of those things mean.

23.2. Ordinary functions

The following code shows three ways of doing (roughly) the same thing: creating an ordinary function.

// 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
};

As we have seen in the chapter on variables, function declarations are hoisted, while variable declarations (e.g. via const) are not. We’ll explore the consequences of that later in this chapter.

The syntax of function declarations and function expressions is very similar. The context determines which is which. For more information on this kind of syntactic ambiguity, consult the chapter on syntax.

23.2.1. Parts of a function declaration

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

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

23.2.2. Names of ordinary functions

The name of a function expression is only accessible inside the function, where the function can use it to refer to itself (e.g. for self-recursion):

const func = function funcExpr() { return funcExpr };
assert.equal(func(), func);
// The name `funcExpr` only exists inside the function:
assert.throws(() => funcExpr, ReferenceError);

In contrast, the name of a function declaration is accessible inside the current scope:

function funcDecl() { return funcDecl }
// The name `funcDecl` exists inside the current scope
assert.equal(funcDecl(), funcDecl);

23.2.3. 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:

23.3. 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.

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

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

23.3.1. Specialized functions are still functions

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

23.3.2. Recommendation: prefer specialized functions

Normally, you should prefer specialized functions over ordinary functions, especially classes and methods. The choice between an arrow function and an ordinary function is less clear-cut, though:

23.3.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).
23.3.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 });
23.3.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:

23.3.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.

This pitfall is caused by syntactic ambiguity: object literals and code blocks have the same syntax and we must help JavaScript with distinguishing them.

23.4. Hoisting functions

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

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

function foo() { return 123; }

Hoisting lets you call foo() before it is declared.

Variable declarations are not hoisted: In the following example, you can only use bar() after its declaration.

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

const bar = () => { return 123; };

assert.equal(bar(), 123); // after declaration 

Class declarations are not hoisted, either:

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

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

23.4.1. Calling ahead without hoisting

Note that a function f() can still call a non-hoisted function g() before its declaration – if f() is invoked after the declaration of g():

const f = () => g();
const g = () => 123;

// We call f() after g() was declared:
assert.equal(f(), 123);

The functions of a module are usually invoked after the complete body of a module was executed. Therefore, you rarely need to worry about the order of functions in a module.

23.4.2. A pitfall of hoisting

If you rely on hoisting to call a function before its declaration then you need to be careful that it doesn’t access non-hoisted data.

hoistedFunc();

const MY_STR = 'abc';
function hoistedFunc() {
  assert.throws(
    () => MY_STR,
    ReferenceError);
}

As before, the problem goes away if you make the function call hoistedFunc() at the end.

23.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);

23.6. Parameter handling

23.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:

23.6.2. Terminology: callback

A callback or callback function is a function that is passed as an argument to another function or a method. This term is used often and broadly in the JavaScript community.

The following is an example of a callback:

const myArray = ['a', 'b'];
const callback = (x) => console.log(x);
myArray.forEach(callback);

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

23.6.3. 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]);

23.6.4. 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]);

23.6.5. 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, []]);
23.6.5.1. Enforcing a certain number of arguments via a rest parameter

You can use a rest parameter to enforce a certain number of arguments. 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;
  // ···
}

23.6.6. 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:

23.6.7. 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 });

23.6.8. 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:

23.6.8.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
23.6.8.2. Example: spreading into Array.prototype.push()

Similarly, the Array method .push() destructively adds its zero or more parameters to the end of its Array. 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

  Quiz

See quiz app.