Please support this book: buy it or donate

23. Callable values



23.1. Kinds of functions

JavaScript has two categories of functions:

  • An ordinary function can play several roles:
    • Real function (in other languages, you’d simply use the term “function”; in JavaScript, we need to distinguish between the role “real function” and the entity “ordinary function” that can play that role)
    • Method
    • Constructor function
  • A specialized function can only play one of those roles. For example:
    • An arrow function can only be a real function.
    • A method can only be a method.
    • A class can only be a constructor function.
      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.

  1. // Function declaration (a statement)
  2. function ordinary1(a, b, c) {
  3. // ···
  4. }
  5. // Anonymous function expression
  6. const ordinary2 = function (a, b, c) {
  7. // ···
  8. };
  9. // Named function expression
  10. const ordinary3 = function myName(a, b, c) {
  11. // `myName` is only accessible in here
  12. };

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:

  1. function add(x, y) {
  2. return x + y;
  3. }
  • add is the name of the function declaration.
  • add(x, y) is the head of the function declaration.
  • x and y are the parameters.
  • The curly braces ({ and }) and everything between them are the body of the function declaration.
  • The return operator explicitly returns a value from the function.

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

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

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

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

23.2.3. Roles played by ordinary functions

Consider the following function declaration from the previous section:

  1. function add(x, y) {
  2. return x + y;
  3. }

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

  • Real function: invoked via a function call. It’s what most programming languages consider to be simply a function.
  1. assert.equal(add(2, 1), 3);
  • Method: stored in property, invoked via a method call.
  1. const obj = { addAsMethod: add };
  2. assert.equal(obj.addAsMethod(2, 4), 6);
  • Constructor function/class: invoked via new.
  1. const inst = new add();
  2. assert.equal(inst instanceof add, true);

(As an aside, the names of classes normally start with capital letters.)

23.3. Specialized functions

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

  • An arrow function can only be a real function:
  1. const arrow = () => { return 123 };
  2. assert.equal(arrow(), 123);
  • A method can only be a method:
  1. const obj = { method() { return 'abc' } };
  2. assert.equal(obj.method(), 'abc');
  • A class can only be a constructor function:
  1. class MyClass { /* ··· */ }
  2. const inst = new MyClass();

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

Table 18: Capabilities of four kinds of functions.
Ordinary functionArrow functionMethodClass
Function call
Method calllexical 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:

  1. > (() => {}) instanceof Function
  2. true
  3. > ({ method() {} }.method) instanceof Function
  4. true
  5. > (class SomeClass {}) instanceof Function
  6. 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:

  • Arrow functions don’t have this as an implicit parameter. That is almost always what you want if you use a real function, because it avoids an important this-related pitfall (for details, consult the chapter on single objects).

  • However, I like the function declaration (which produces an ordinary function) syntactically. If you don’t use this inside it, it is mostly equivalent to const plus arrow function:

  1. function funcDecl(x, y) {
  2. return x * y;
  3. }
  4. const arrowFunc = (x, y) => {
  5. return x * y;
  6. };

23.3.3. Arrow functions

Arrow functions were added to JavaScript for two reasons:

  • To provide a more concise way for creating functions.
  • 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:

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

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

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

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

  1. const id = x => x;

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

  1. > [1,2,3].map(x => x+1)
  2. [ 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. [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:

  • As each ordinary function can be a method, it has its own this.
  • That own this makes it impossible to access the this of the surrounding scope from inside an ordinary function. And that is inconvenient for real functions.
    The following code demonstrates a common work-around:
  1. const prefixer = {
  2. prefix: '==> ',
  3. prefixStringArray(stringArray) {
  4. const that = this; // (A)
  5. return stringArray.map(
  6. function (x) {
  7. return that.prefix + x; // (B)
  8. });
  9. },
  10. };
  11. assert.deepEqual(
  12. prefixer.prefixStringArray(['a', 'b']),
  13. ['==> 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.

  1. const prefixer = {
  2. prefix: '==> ',
  3. prefixStringArray(stringArray) {
  4. return stringArray.map(
  5. x => this.prefix + x);
  6. },
  7. };

To summarize:

  • In ordinary functions, this is an implicit (dynamic) parameter (details in the chapter on single objects).
  • Arrow functions get this from their surrounding scopes (lexically).
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:

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

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

  1. const func2 = () => {a: 1};
  2. 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):

  1. assert.equal(foo(), 123); // OK
  2. 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.

  1. assert.throws(
  2. () => bar(), // before declaration
  3. ReferenceError);
  4. const bar = () => { return 123; };
  5. assert.equal(bar(), 123); // after declaration

Class declarations are not hoisted, either:

  1. assert.throws(
  2. () => new MyClass(),
  3. ReferenceError);
  4. class MyClass {}
  5. 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():

  1. const f = () => g();
  2. const g = () => 123;
  3. // We call f() after g() was declared:
  4. 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.

  1. hoistedFunc();
  2. const MY_STR = 'abc';
  3. function hoistedFunc() {
  4. assert.throws(
  5. () => MY_STR,
  6. ReferenceError);
  7. }

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:

  1. function func() {
  2. return 123;
  3. }
  4. assert.equal(func(), 123);

Another example:

  1. function boolToYesNo(bool) {
  2. if (bool) {
  3. return 'Yes';
  4. } else {
  5. return 'No';
  6. }
  7. }
  8. assert.equal(boolToYesNo(true), 'Yes');
  9. assert.equal(boolToYesNo(false), 'No');

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

  1. function noReturn() {
  2. // No explicit return
  3. }
  4. 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:

  • Parameters are part of a function definition. They are also called formal parameters and formal arguments.

  • Arguments are part of a function call. They are also called actual parameters and actual arguments.

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:

  1. const myArray = ['a', 'b'];
  2. const callback = (x) => console.log(x);
  3. myArray.forEach(callback);
  4. // Output:
  5. // 'a'
  6. // '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:

  • Extra arguments are ignored.
  • Missing parameters are set to undefined.
    For example:
  1. function foo(x, y) {
  2. return [x, y];
  3. }
  4. // Too many arguments:
  5. assert.deepEqual(foo('a', 'b', 'c'), ['a', 'b']);
  6. // The expected number of arguments:
  7. assert.deepEqual(foo('a', 'b'), ['a', 'b']);
  8. // Not enough arguments:
  9. 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:

  1. function f(x, y=0) {
  2. return [x, y];
  3. }
  4. assert.deepEqual(f(1), [1, 0]);
  5. assert.deepEqual(f(), [undefined, 0]);

undefined also triggers the default value:

  1. assert.deepEqual(
  2. f(undefined, undefined),
  3. [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:

  1. function f(x, ...y) {
  2. return [x, y];
  3. }
  4. assert.deepEqual(
  5. f('a', 'b', 'c'),
  6. ['a', ['b', 'c']]);
  7. assert.deepEqual(
  8. f(),
  9. [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.

  1. function bar(a, b) {
  2. // ···
  3. }

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

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

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:

  • 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.
  1. selectEntries(3, 20, 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:
  1. selectEntries({start: 3, end: 20, step: 2})

Named parameters have several benefits:

  • They lead to more self-explanatory code, because each argument has a descriptive label. Just compare the two versions of selectEntries(): With the second one, it is much easier to see what happens.

  • Order of parameters doesn’t matter (as long as the names are correct).

  • Handling more than one optional parameter is more convenient: Callers can easily provide any subset of all optional parameters and don’t have to be aware of the ones they omitted (with positional parameters, you have to fill in preceding optional parameters, with undefined).

23.6.7. Simulating named parameters

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

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

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

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

This destructuring pattern works for empty object literals:

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

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

  1. > selectEntries()
  2. 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.

  1. function selectEntries({start=0, end=-1, step=1} = {}) {
  2. return {start, end, step};
  3. }
  4. assert.deepEqual(
  5. selectEntries(),
  6. { 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:

  1. function func(x, y) {
  2. console.log(x);
  3. console.log(y);
  4. }
  5. const someIterable = ['a', 'b'];
  6. func(...someIterable);
  7. // Output:
  8. // 'a'
  9. // 'b'

Therefore, spread arguments and rest parameters serve opposite purposes:

  • Rest parameters are used when defining functions or methods. They collect arguments in Arrays.
  • Spread arguments are used when calling functions or methods. They turn iterable objects into arguments.
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:

  1. > Math.max(-1, 5, 11, 3)
  2. 11
  3. > Math.max(...[-1, 5, 11, 3])
  4. 11
  5. > Math.max(-1, ...[-5, 11], 3)
  6. 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:

  1. const arr1 = ['a', 'b'];
  2. const arr2 = ['c', 'd'];
  3. arr1.push(...arr2);
  4. assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);