HomepageExploring JavaScript (ES2024 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

27 Callable values

In this chapter, we look at JavaScript values that can be invoked: functions, methods, and classes.

27.1 Kinds of functions

JavaScript has two categories of functions:

Read on to find out what all of those things mean.

27.2 Ordinary functions

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

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

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

Inside a scope, function declarations are activated early (see “Declarations: scope and activation” (§13.8)) and can be called before they are declared. That is occasionally useful.

Variable declarations, such as the one for ordinary2, are not activated early.

27.2.1 Named function expressions (advanced)

So far, we have only seen anonymous function expressions – which don’t have names:

const anonFuncExpr = function (a, b, c) {
  // ···
};

But there are also named function expressions:

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

myName is only accessible inside the body of the function. The function can use it to refer to itself (for self-recursion, etc.) – independently of which variable it is assigned to:

const func = function funcExpr() { return funcExpr };
assert.equal(func(), func);

// The name `funcExpr` only exists inside the function body:
assert.throws(() => funcExpr(), ReferenceError);

Even if they are not assigned to variables, named function expressions have names (line A):

function getNameOfCallback(callback) {
  return callback.name;
}

assert.equal(
  getNameOfCallback(function () {}), ''); // anonymous

assert.equal(
  getNameOfCallback(function named() {}), 'named'); // (A)

Note that functions created via function declarations or variable declarations always have names:

function funcDecl() {}
assert.equal(
  getNameOfCallback(funcDecl), 'funcDecl');

const funcExpr = function () {};
assert.equal(
  getNameOfCallback(funcExpr), 'funcExpr');

One benefit of functions having names is that those names show up in error stack traces.

27.2.2 Terminology: function definitions and function expressions

A function definition is syntax that creates functions:

Function declarations always produce ordinary functions. Function expressions produce either ordinary functions or specialized functions:

While function declarations are still popular in JavaScript, function expressions are almost always arrow functions in modern code.

27.2.3 Parts of a function declaration

Let’s examine the parts of a function declaration via the following example. Most of the terms also apply to function expressions.

function add(x, y) {
  return x + y;
}
27.2.3.1 Trailing commas in parameter lists [ES2017]

JavaScript has always allowed and ignored trailing commas in Array literals. Since ES5, they are also allowed in object literals. Since ES2017, we can add trailing commas to parameter lists (declarations and invocations):

// Declaration
function retrieveData(
  contentText,
  keyword,
  {unique, ignoreCase, pageSize}, // trailing comma
) {
  // ···
}

// Invocation
retrieveData(
  '',
  null,
  {ignoreCase: true, pageSize: 10}, // trailing comma
);

27.2.4 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:

27.2.5 Terminology: entity vs. syntax vs. role (advanced)

The distinction between the concepts syntax, entity, and role is subtle and often doesn’t matter. But I’d like to sharpen your eye for it:

Many other programming languages only have a single entity that plays the role real function. Then they can use the name function for both role and entity.

27.3 Specialized functions [ES6]

Specialized functions are single-purpose versions of ordinary functions. Each one of them specializes in a single role:

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

Table 27.1 lists the capabilities of ordinary and specialized functions.

Function callMethod callConstructor call
Ordinary function(this === undefined)
Arrow function(lexical this)
Method(this === undefined)
Class

Table 27.1: Capabilities of four kinds of functions. If a cell value is in parentheses, that implies some kind of limitation. The special variable this is explained in “The special variable this in methods, ordinary functions and arrow functions” (§27.3.3).

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

27.3.2 Arrow functions

Arrow functions were added to JavaScript for two reasons:

  1. To provide a more concise way for creating functions.
  2. They work better as real functions inside methods: Methods can refer to the object that received a method call via the special variable this. Arrow functions can access the this of a surrounding method, ordinary functions can’t (because they have their own this).

We’ll first examine the syntax of arrow functions and then how this works in various functions.

27.3.2.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 previous example demonstrates one benefit of arrow functions – conciseness. If we perform the same task with a function expression, our code is more verbose:

[1,2,3].map(function (x) { return x+1 });
27.3.2.2 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. Without an explicit return statement, the block body returns undefined.

This pitfall is caused by syntactic ambiguity: object literals and code blocks have the same syntax. We use the parentheses to tell JavaScript that the body is an expression (an object literal) and not a statement (a block).

27.3.3 The special variable this in methods, ordinary functions and arrow functions

Icon “reading”The special variable this is an object-oriented feature

We are taking a quick look at the special variable this here, in order to understand why arrow functions are better real functions than ordinary functions.

But this feature only matters in object-oriented programming and is covered in more depth in “Methods and the special variable this” (§30.5). Therefore, don’t worry if you don’t fully understand it yet.

Inside methods, the special variable this lets us access the receiver – the object which received the method call:

const obj = {
  myMethod() {
    assert.equal(this, obj);
  }
};
obj.myMethod();

Ordinary functions can be methods and therefore also have the implicit parameter this:

const obj = {
  myMethod: function () {
    assert.equal(this, obj);
  }
};
obj.myMethod();

this is even an implicit parameter when we use an ordinary function as a real function. Then its value is undefined (if strict mode is active, which it almost always is):

function ordinaryFunc() {
  assert.equal(this, undefined);
}
ordinaryFunc();

That means that an ordinary function, used as a real function, can’t access the this of a surrounding method (line A). In contrast, arrow functions don’t have this as an implicit parameter. They treat it like any other variable and can therefore access the this of a surrounding method (line B):

const jill = {
  name: 'Jill',
  someMethod() {
    function ordinaryFunc() {
      assert.throws(
        () => this.name, // (A)
        /^TypeError: Cannot read properties of undefined \(reading 'name'\)$/);
    }
    ordinaryFunc();

    const arrowFunc = () => {
      assert.equal(this.name, 'Jill'); // (B)
    };
    arrowFunc();
  },
};
jill.someMethod();

In this code, we can observe two ways of handling this:

27.3.4 Recommendation: prefer specialized functions over ordinary functions

Normally, you should prefer specialized functions over ordinary functions, especially classes and methods.

When it comes to real functions, the choice between an arrow function and an ordinary function is less clear-cut, though:

27.4 Summary: kinds of callable values

Icon “reading”This section refers to upcoming content

This section mainly serves as a reference for the current and upcoming chapters. Don’t worry if you don’t understand everything.

So far, all (real) functions and methods, that we have seen, were:

Later chapters will cover other modes of programming:

These modes can be combined – for example, there are synchronous iterables and asynchronous iterables.

Several new kinds of functions and methods help with some of the mode combinations:

That leaves us with 4 kinds (2 × 2) of functions and methods:

Table 27.2 gives an overview of the syntax for creating these 4 kinds of functions and methods.

Result#
Sync functionSync method
function f() {}{ m() {} }value1
f = function () {}
f = () => {}
Sync generator functionSync gen. method
function* f() {}{ * m() {} }iterable0+
f = function* () {}
Async functionAsync method
async function f() {}{ async m() {} }Promise1
f = async function () {}
f = async () => {}
Async generator functionAsync gen. method
async function* f() {}{ async * m() {} }async iterable0+
f = async function* () {}

Table 27.2: Syntax for creating functions and methods. The last column specifies how many values are produced by an entity.

27.5 Returning values from functions and methods

(Everything mentioned in this section applies to both functions and methods.)

The return statement explicitly returns a value 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);

27.6 Parameter handling

Once again, I am only mentioning functions in this section, but everything also applies to methods.

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

27.6.2 Terminology: callback

A callback or callback function is a function that is an argument of a function or method call.

The following is an example of a callback:

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

Output:

a
b

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

27.6.4 Parameter default values [ES6]

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

27.6.5 Rest parameters [ES6]

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, []]
);

There are two restrictions related to how we can use rest parameters:

27.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 createPoint(x, y) {
  return {x, y};
    // same as {x: x, y: y}
}

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

function createPoint(...args) {
  if (args.length !== 2) {
    throw new Error('Please provide exactly 2 arguments!');
  }
  const [x, y] = args; // (A)
  return {x, y};
}

In line A, we access the elements of args via destructuring.

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

    selectEntries(3, 20, 2)
    
  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:

    selectEntries({start: 3, end: 20, step: 2})
    

Named parameters have several benefits:

27.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 read properties of undefined (reading 'start')

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

27.6.8 Spreading (...) into function calls [ES6]

If you put three dots (...) in front of the argument of a function call, then you spread it. That means that the argument must be an iterable object and the iterated values all become arguments. In other words, a single argument is expanded into multiple arguments – for example:

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

Output:

a
b

Spreading and rest parameters use the same syntax (...), but they serve opposite purposes:

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

Icon “exercise”Exercises: Parameter handling

27.7 Methods of functions: .call(), .apply(), .bind()

Functions are objects and have methods. In this section, we look at three of those methods: .call(), .apply(), and .bind().

27.7.1 The function method .call()

Each function someFunc has the following method:

someFunc.call(thisValue, arg1, arg2, arg3);

This method invocation is loosely equivalent to the following function call:

someFunc(arg1, arg2, arg3);

However, with .call(), we can also specify a value for the implicit parameter this. In other words: .call() makes the implicit parameter this explicit.

The following code demonstrates the use of .call():

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

assert.deepEqual(
  func.call('hello', 'a', 'b'),
  ['hello', 'a', 'b']);

As we have seen before, if we function-call an ordinary function, its this is undefined:

assert.deepEqual(
  func('a', 'b'),
  [undefined, 'a', 'b']);

Therefore, the previous function call is equivalent to:

assert.deepEqual(
  func.call(undefined, 'a', 'b'),
  [undefined, 'a', 'b']);

In arrow functions, the value for this provided via .call() (or other means) is ignored.

27.7.2 The function method .apply()

Each function someFunc has the following method:

someFunc.apply(thisValue, [arg1, arg2, arg3]);

This method invocation is loosely equivalent to the following function call (which uses spreading):

someFunc(...[arg1, arg2, arg3]);

However, with .apply(), we can also specify a value for the implicit parameter this.

The following code demonstrates the use of .apply():

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

const args = ['a', 'b'];
assert.deepEqual(
  func.apply('hello', args),
  ['hello', 'a', 'b']);

27.7.3 The function method .bind()

.bind() is another method of function objects. This method is invoked as follows:

const boundFunc = someFunc.bind(thisValue, arg1, arg2);

.bind() returns a new function boundFunc(). Calling that function invokes someFunc() with this set to thisValue and these parameters: arg1, arg2, followed by the parameters of boundFunc().

That is, the following two function calls are equivalent:

boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, 'a', 'b')
27.7.3.1 An alternative to .bind()

Another way of pre-filling this and parameters is via an arrow function:

const boundFunc2 = (...args) =>
  someFunc.call(thisValue, arg1, arg2, ...args);
27.7.3.2 An implementation of .bind()

Considering the previous section, .bind() can be implemented as a real function as follows:

function bind(func, thisValue, ...boundArgs) {
  return (...args) =>
    func.call(thisValue, ...boundArgs, ...args);
}
27.7.3.3 Example: binding a real function

Using .bind() for real functions is somewhat unintuitive because we have to provide a value for this. Given that it is undefined during function calls, it is usually set to undefined or null.

In the following example, we create add8(), a function that has one parameter, by binding the first parameter of add() to 8.

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

const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9);