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.
As we have seen in , function declarations are hoisted, while variable declarations (e.g. via ) 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:
add
is the name of the function declaration.add(x, y)
is the head of the function declaration.x
andy
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):
In contrast, the name of a function declaration is accessible inside the current scope:
23.2.3. Roles played by ordinary functions
Consider the following function declaration from the previous section:
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.
- Method: stored in property, invoked via a method call.
- Constructor function/class: invoked via
new
.
(As an aside, the names of classes normally start with capital letters.)
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:
- A method can only be a method:
- A class can only be a constructor function:
Apart from nicer syntax, each kind of specialized function also supports new features, making them better at their job than ordinary functions.
- Arrow functions are explained .
- Methods are explained in the chapter on single objects.
- Classes are explained in .
Tbl. 18 lists the capabilities of ordinary and specialized functions.
23.3.1. Specialized functions are still functions
It’s important to note that arrow functions, methods and classes are still categorized as functions:
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 importantthis
-related pitfall (for details, consult ).However, I like the function declaration (which produces an ordinary function) syntactically. If you don’t use
this
inside it, it is mostly equivalent toconst
plus arrow function:
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:
The (roughly) equivalent arrow function looks as follows. Arrow functions are expressions.
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.
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:
This last example demonstrates the first benefit of arrow functions – conciseness. In contrast, this is the same method call, but with a function expression:
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 thethis
of the surrounding scope from inside an ordinary function. And that is inconvenient for real functions.
The following code demonstrates a common work-around:
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.
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:
If you don’t, JavaScript thinks, the arrow function has a block body (that doesn’t return anything):
{a: 1}
is interpreted as a block with the label a:
and the expression statement 1
.
This pitfall is caused by : 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):
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.
Class declarations are not hoisted, either:
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()
:
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.
As before, the problem goes away if you make the function call hoistedFunc()
at the end.
You use the return
operator to return values from a function:
Another example:
If, at the end of a function, you haven’t returned anything explicitly, JavaScript returns undefined
for you:
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.
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:
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:
23.6.4. Parameter default values
Parameter default values specify the value to use if a parameter has not been provided. For example:
undefined
also triggers the default value:
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:
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.
This is how we force callers to always provide two arguments:
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.
- 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:
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:
This function uses destructuring to access the properties of its single parameter. The pattern it uses is an abbreviation for the following pattern:
This destructuring pattern works for empty object literals:
But it does not work if you call the function without any parameters:
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.
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:
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()
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: