In this chapter, we look at JavaScript values that can be invoked: functions, methods, and classes.
JavaScript has two categories of functions:
An ordinary function can play several roles:
A specialized function can only play one of those roles – for example:
Specialized functions were added to the language in ECMAScript 6.
Read on to find out what all of those things mean.
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.
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.
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.
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;
}
add
is the name of the function declaration.
add(x, y)
is the head of the function declaration.
x
and y
are the parameters.
{
and }
) and everything between them are the body of the function declaration.
return
statement explicitly returns a value from the function.
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
);
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:
Real function: invoked via a function call.
assert.equal(add(2, 1), 3);
Method: stored in a property, invoked via a method call.
const obj = { addAsMethod: add };
assert.equal(obj.addAsMethod(2, 4), 6); // (A)
In line A, obj
is called the receiver of the method call.
Constructor function: invoked via new
.
const inst = new add();
assert.equal(inst instanceof add, true);
As an aside, the names of constructor functions (incl. classes) normally start with capital letters.
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:
An entity is a JavaScript feature as it “lives” in RAM. An ordinary function is an entity.
Syntax is the code that we use to create entities. Function declarations and anonymous function expressions are syntax. They both create entities that are called ordinary functions.
A role describes how we use entities. The entity ordinary function can play the role real function, or the role method, or the role class. The entity arrow function can also play the role real function.
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.
Specialized functions are single-purpose versions of ordinary functions. Each one of them specializes in a single role:
The purpose of an arrow function is to be a real function:
const arrow = () => {
return 123;
};
assert.equal(arrow(), 123);
The purpose of a method is to be a method:
const obj = {
myMethod() {
return 'abc';
}
};
assert.equal(obj.myMethod(), 'abc');
The purpose of a class is to be a constructor function:
class MyClass {
/* ··· */
}
const inst = new MyClass();
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 call | Method call | Constructor 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).
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
Arrow functions were added to JavaScript for two reasons:
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.
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 });
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).
this
in methods, ordinary functions and arrow functions 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
:
Dynamic this
: In line A, we try to access the this
of .someMethod()
from an ordinary function. There, it is shadowed by the function’s own this
, which is undefined
(as filled in by the function call). Given that ordinary functions receive their this
via (dynamic) function or method calls, their this
is called dynamic.
Lexical this
: In line B, we again try to access the this
of .someMethod()
. This time, we succeed because the arrow function does not have its own this
. this
is resolved lexically, just like any other variable. That’s why the this
of arrow functions is called lexical.
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:
For anonymous inline function expressions, arrow functions are clear winners, due to their compact syntax and them not having this
as an implicit parameter:
const twiceOrdinary = [1, 2, 3].map(function (x) {return x * 2});
const twiceArrow = [1, 2, 3].map(x => x * 2);
For stand-alone named function declarations, arrow functions still benefit from lexical this
. But function declarations (which produce ordinary functions) have nice syntax and early activation is also occasionally useful (see “Declarations: scope and activation” (§13.8)). If this
doesn’t appear in the body of an ordinary function, there is no downside to using it as a real function. The static checking tool ESLint can warn us during development when we do this wrong via a built-in rule.
function timesOrdinary(x, y) {
return x * y;
}
const timesArrow = (x, y) => {
return x * y;
};
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 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* () {} |
Table 27.2: Syntax for creating functions and methods. The last column specifies how many values are produced by an entity.
(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);
Once again, I am only mentioning functions in this section, but everything also applies to methods.
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.
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 does not complain if a function call provides a different number of arguments than expected by the function definition:
undefined
.
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]);
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]);
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:
We cannot use more than one rest parameter per function definition.
assert.throws(
() => eval('function f(...x, ...y) {}'),
/^SyntaxError: Rest parameter must be last formal parameter$/
);
A rest parameter must always come last. As a consequence, we can’t access the last parameter like this:
assert.throws(
() => eval('function f(...restParams, lastParam) {}'),
/^SyntaxError: Rest parameter must be last formal 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.
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.
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:
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.
The order of the arguments 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 omit (with positional parameters, you have to fill in preceding optional parameters, with undefined
).
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 });
...
) 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:
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
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
exercises/callables/positional_parameters_test.mjs
exercises/callables/named_parameters_test.mjs
.call()
, .apply()
, .bind()
Functions are objects and have methods. In this section, we look at three of those methods: .call()
, .apply()
, and .bind()
.
.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.
.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']);
.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')
.bind()
Another way of pre-filling this
and parameters is via an arrow function:
const boundFunc2 = (...args) =>
someFunc.call(thisValue, arg1, arg2, ...args);
.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);
}
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);