JavaScript for impatient programmers
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 two 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) {
  // ···
}

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

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

As we have seen in §10.8 “Declarations: scope and activation”, function declarations are activated early, while variable declarations (e.g., via const) are not.

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 §6.5 “Ambiguous 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 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:

  Ordinary function vs. real function

In JavaScript, we distinguish:

In many other programming languages, the entity function only plays one role – function. Therefore, the same name function can be used for both.

23.2.3 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 in the current scope
assert.equal(funcDecl(), funcDecl);

23.3 Specialized functions

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.

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

Table 15: Capabilities of four kinds of functions. “Implicit this” means that this is an implicit parameter.
Ordinary function Arrow function Method Class
Function call implicit this implicit this
Method call
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.

Next, we’ll first look at the syntax of arrow functions and then how they help with this.

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 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 });
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 the issue:

const person = {
  name: 'Jill',
  someMethod() {
    const ordinaryFunc = function () {
      assert.throws(
        () => this.name, // (A)
        /^TypeError: Cannot read property 'name' of undefined$/);
    };
    const arrowFunc = () => {
      assert.equal(this.name, 'Jill'); // (B)
    };
    
    ordinaryFunc();
    arrowFunc();
  },
}

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

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

For more information on shadowing this, consult §25.4.5 “this pitfall: accidentally shadowing this.

23.4 More kinds of functions and methods

  This section is a summary of 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:

Tbl. 16 gives an overview of the syntax for creating these 4 kinds of functions and methods.

Table 16: Syntax for creating functions and methods. The last column specifies how many values are produced by an entity.
Result Values
Sync function Sync method
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
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* () {}

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

23.6 Parameter handling

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

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 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'

  JavaScript uses the term callback broadly

In other programming languages, the term callback often has a narrower meaning: it refers to a pattern for delivering results asynchronously, via a function-valued parameter. In this meaning, the callback (or continuation) is invoked after a function has completely finished its computation.

Callbacks as an asynchronous pattern, are described in the chapter on asynchronous programming.

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

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

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:

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

23.7 Dynamically evaluating code: eval(), new Function() (advanced)

Next, we’ll look at two ways of evaluating code dynamically: eval() and new Function().

23.7.1 eval()

Given a string str with JavaScript code, eval(str) evaluates that code and returns the result:

> eval('2 ** 4')
16

There are two ways of invoking eval():

“Not via a function call” means “anything that looks different than eval(···)”:

The following code illustrates the difference:

globalThis.myVariable = 'global';
function func() {
  const myVariable = 'local';
  
  // Direct eval
  assert.equal(eval('myVariable'), 'local');
  
  // Indirect eval
  assert.equal(eval.call(undefined, 'myVariable'), 'global');
}

Evaluating code in global context is safer because the code has access to fewer internals.

23.7.2 new Function()

new Function() creates a function object and is invoked as follows:

const func = new Function('«param_1»', ···, '«param_n»', '«func_body»');

The previous statement is equivalent to the next statement. Note that «param_1», etc., are not inside string literals, anymore.

const func = function («param_1», ···, «param_n») {
  «func_body»
};

In the next example, we create the same function twice, first via new Function(), then via a function expression:

const times1 = new Function('a', 'b', 'return a * b');
const times2 = function (a, b) { return a * b };

  new Function() creates non-strict mode functions

Functions created via new Function() are sloppy.

23.7.3 Recommendations

Avoid dynamic evaluation of code as much as you can:

Very often, JavaScript is dynamic enough so that you don’t need eval() or similar. In the following example, what we are doing with eval() (line A) can be achieved just as well without it (line B).

const obj = {a: 1, b: 2};
const propKey = 'b';

assert.equal(eval('obj.' + propKey), 2); // (A)
assert.equal(obj[propKey], 2); // (B)

If you have to dynamically evaluate code:

  Quiz

See quiz app.