...
) into function callseval()
and new Function()
eval()
new Function()
JavaScript has two categories of functions:
The next two sections explain what all of those things mean.
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”.
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
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.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:
Constructor function/class: invoked via new
.
(As an aside, the names of classes normally start with capital letters.)
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.
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);
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:
The purpose of a method is to be a method:
The purpose of a class is to be a constructor function:
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.
Ordinary function | Arrow function | Method | Class | |
---|---|---|---|---|
Function call | implicit this |
✔ |
implicit this |
✘ |
Method call | ✔ |
✘ |
✔ |
✘ |
Constructor call | ✔ |
✘ |
✘ |
✔ |
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
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 §25.4.6 “Avoiding the pitfalls of this
”).
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:
Arrow functions were added to JavaScript for two reasons:
this
of the surrounding scope inside an ordinary function (details soon).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:
That is convenient when passing arrow functions as parameters to other functions or methods:
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:
this
Ordinary functions can be both methods and real functions. Alas, the two roles are in conflict:
this
.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 this 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
:
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
(due 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.
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
. 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
”.
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.
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* () {} |
(Everything mentioned in this section applies to both functions and methods.)
The return
statement explicitly returns a value from a function:
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:
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 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.
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:
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, []]);
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:
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.
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.
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:
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.
function selectEntries({start=0, end=-1, step=1} = {}) {
return {start, end, step};
}
assert.deepEqual(
selectEntries(),
{ start: 0, end: -1, step: 1 });
...
) into function callsIf 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:
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
eval()
and new Function()
Next, we’ll look at two ways of evaluating code dynamically: eval()
and new Function()
.
eval()
Given a string str
with JavaScript code, eval(str)
evaluates that code and returns the result:
There are two ways of invoking eval()
:
“Not via a function call” means “anything that looks different than eval(···)
”:
eval.call(undefined, '···')
(0, eval)('···')
(uses the comma operator)window.eval('···')
const e = eval; e('···')
The following code illustrates the difference:
window.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 then the code has access to fewer internals.
new Function()
new Function()
creates a function object and is invoked as follows:
The previous statement is equivalent to the next statement. Note that «param_1»
(etc.) are not inside string literals, anymore.
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.
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:
new Function()
over eval()
: It always executes its code in global context and a function provides a clean interface to the evaluated code.eval
over direct eval
: Evaluating code in global context is safer. Quiz
See quiz app.