Topics in This Chapter
3.5 Functional Array Processing
3.10 Supplying More or Fewer Arguments
3.12 Rest Parameters and the Spread Operator
In this chapter, you will learn how to write functions in JavaScript. JavaScript is a “functional” programming language. Functions are “first-class” values, just like numbers or strings. Functions can consume and produce other functions. Mastering a functional programming style is essential for working with modern JavaScript.
This chapter also covers the JavaScript parameter passing and scope rules, as well as the details of throwing and catching exceptions.
In JavaScript, you declare a function by providing
The name of the function
The names of the parameters
The body of the function, which computes and returns the function result
You do not specify the types of the function parameters or result. Here is an example:
function average(x, y) { return (x + y) / 2 }
The return
statement yields the value that the function returns.
To call this function, simply pass the desired arguments:
let result = average(6, 7) // result
is set to 6.5
What if you pass something other than a number? Whatever happens, happens. For example:
result = average('6', '7') // result
is set to 33.5
When you pass strings, the +
in the function body concatenates them. The resulting string '67'
is converted to a number before the division by 2.
That looks rather casual to a Java, C#, or C++ programmer who is used to compile-time type checking. Indeed, if you mess up argument types, you only find out when something strange happens at runtime. On the flip side, you can write functions that work with arguments of multiple types, which can be convenient.
The return
statement returns immediately, abandoning the remainder of the function. Consider this example—an indexOf
function that computes the index of a value in an array:
function indexOf(arr, value) { for (let i in arr) { if (arr[i] === value) return i } return -1 }
As soon as a match is found, the index is returned and the function terminates.
A function may choose not to specify a return value. If the function body exits without a return
statement, or a return
keyword isn’t followed by an expression, the function returns the undefined
value. This usually happens when a function is solely called for a side effect.
If a function sometimes returns a result, and sometimes you don’t want to return anything, be explicit:
return undefined
As mentioned in Chapter 2, a return
statement must always have at least one token before the end of the line, to avoid automatic semicolon insertion. For example, if a function returns an object, put at least the opening brace on the same line:
return { average: (x + y) / 2, max: Math.max(x, y), . . . }
JavaScript is a functional programming language. Functions are values that you can store in variables, pass as arguments, or return as function results.
For example, we can store the average
function in a variable:
let f = average
Then you can call the function:
let result = f(6, 7)
When the expression f(6, 7)
is executed, the contents of f
is found to be a function. That function is called with arguments 6
and 7
.
We can later put another function into the variable f
:
f = Math.max
Now when you compute f(6, 7)
, the answer becomes 7
, the result of calling Math.max
with the provided arguments.
Here is an example of passing a function as an argument. If arr
is an array, the method call
arr.map(someFunction)
applies the provided function to all elements, and returns an array of the collected results (without modifying the original array). For example,
result = [0, 1, 2, 4].map(Math.sqrt)
sets result
to
[0, 1, 1.4142135623730951, 2]
The map
method is sometimes called a higher-order function: a function that consumes another function.
Let us continue the example of the preceding section. Suppose we want to multiply all array elements by 10. Of course, we can write a function
function multiplyBy10(x) { return x * 10 }
Now we can call:
result = [0, 1, 2, 4].map(multiplyBy10)
But it seems a waste to declare a new function just to use it once.
It is better to use a function literal. JavaScript has two syntactical variants. Here is the first one:
result = [0, 1, 2, 4].map(function (x) { return 10 * x })
The syntax is straightforward. You use the same function
syntax as before, but now you omit the name. The function literal is a value that denotes the function with the specified action. That value is passed to the map
method.
By itself, the function literal doesn’t have a name, just like the array literal [0, 1, 2, 4]
doesn’t have a name. If you want to give the function a name, do what you always do when you want to give something a name—store it in a variable:
const average = function (x, y) { return (x + y) / 2 }
Think of anonymous function literals as the “normal” case. A named function is a shorthand for declaring a function literal and then giving it a name.
In the preceding section, you saw how to declare function literals with the function
keyword. There is a second, more concise form that uses the =>
operator, usually called “arrow”:
const average = (x, y) => (x + y) / 2
You provide the parameter variables to the left of the arrow and the return value to the right.
If there is a single parameter, you don’t need to enclose it in parentheses:
const multiplyBy10 = x => x * 10
If the function has no parameters, use an empty set of parentheses:
const dieToss = () => Math.trunc(Math.random() * 6) + 1
Note that dieToss
is a function, not a number. Each time you call dieToss()
, you get a random integer between 1 and 6.
If an arrow function is more complex, place its body inside a block statement. Use the return
keyword to return a value out of the block:
const indexOf = (arr, value) => { for (let i in arr) { if (arr[i] === value) return i } return -1 }
The =>
token must be on the same line as the parameters:
const average = (x, y) => // OK (x + y) / 2 const distance = (x, y) // Error => Math.abs(x - y)
If you write an arrow function on more than one line, it is clearer to use braces:
const average = (x, y) => { return (x + y) / 2 }
If an arrow function does nothing but returns an object literal, then you must enclose the object in parentheses:
const stats = (x, y) => ({ average: (x + y) / 2, distance: Math.abs(x - y) })
Otherwise, the braces would be parsed as a block.
As you will see in Chapter 4, arrow functions have more regular behavior than functions declared with the function
keyword. Many JavaScript programmers prefer to use the arrow syntax for anonymous and nested functions. Some programmers use the arrow syntax for all functions, while others prefer to declare top-level functions with function
. This is purely a matter of taste.
Instead of iterating over an array with a for of
or for in
loop, you can use the forEach
method. Pass a function that processes the elements and index values:
arr.forEach((element, index) => { console.log(`${index}: ${element}`) })
The function is called for each array element, in increasing index order.
If you only care about the elements, you can pass a function with one parameter:
arr.forEach(element => { console.log(`${element}`) })
The forEach
method will call this function with both the element and the index, but in this example, the index is ignored.
The forEach
method doesn’t produce a result. Instead, the function that you pass to it must have some side effect—printing a value or making an assignment. It is even better if you can avoid side effects altogether and use methods such as map
and filter
that transform arrays into their desired form.
In Section 3.2, “Higher-Order Functions” (page 53), you saw the map
method that transforms an array, applying a function to each element. Here is a practical example. Suppose you want to build an HTML list of items in an array. You can first enclose each of the items in a li
element:
const enclose = (tag, contents) => `<${tag}>${contents}</${tag}>` const listItems = items.map(i => enclose('li', i))
Actually, it is safer to first escape &
and <
characters in the items. Let’s suppose we have an htmlEscape
function for this purpose. (You will find an implementation in the book’s companion code.) Then we can first transform the items to make them safe, and then enclose them:
const listItems = items .map(htmlEscape) .map(i => enclose('li', i))
Now the result is an array of li
elements. Next, we concatenate all strings with the Array.join
method (see Chapter 7), and enclose the resulting string in a ul
element:
const list = enclose('ul', items .map(htmlEscape) .map(i => enclose('li', i)) .join(''))
Another useful array method is filter
. It receives a predicate function—a function that returns a Boolean (or Boolish) value. The result is an array of all elements that fulfill the predicate. Continuing the preceding example, we don’t want to include empty strings in the list. We can remove them like this:
const list = enclose('ul', items .filter(i => i.trim() !== '') .map(htmlEscape) .map(i => enclose('li', i)) .join(''))
This processing pipeline is a good example of a high-level “what, not how” style of programming. What do we want? Throw away empty strings, escape HTML, enclose items in li
elements, and join them. How is this done? Ultimately, by a sequence of loops and branches, but that is an implementation detail.
The setTimeout
function takes two arguments: a function to execute later, when a timeout has elapsed, and the duration of the timeout in milliseconds. For example, this call says “Goodbye” in ten seconds:
setTimeout(() => console.log('Goodbye'), 10000)
Let’s make this more flexible:
const sayLater = (text, when) => { let task = () => console.log(text) setTimeout(task, when) }
Now we can call:
sayLater('Hello', 1000) sayLater('Goodbye', 10000)
Look at the variable text
inside the arrow function () => console.log(text)
. If you think about it, something nonobvious is going on. The code of the arrow function runs long after the call to sayLater
has returned. How does the text
variable stay around? And how can it be first 'Hello'
and then 'Goodbye'
?
To understand what is happening, we need to refine our understanding of a function. A function has three ingredients:
A block of code
Parameters
The free variables—that is, the variables that are used in the code but are not declared as parameters or local variables
A function with free variables is called a closure.
In our example, text
is a free variable of the arrow function. The data structure representing the closure stores a reference to the variable when the function is created. We say that the variable is captured. That way, its value is available when the function is later called.
In fact, the arrow function () => console.log(text)
also captures a second variable, namely console
.
But how does text
get to have two different values? Let’s do this in slow motion. The first call to sayLater
creates a closure that captures the text
parameter variable holding the value 'Hello'
. When the sayLater
method exits, that variable does not go away because it is still used by the closure. When sayLater
is called again, a second closure is created that captures a different text
parameter variable, this time holding 'Goodbye'
.
In JavaScript, a captured variable is a reference to another variable, not its current value. If you change the contents of the captured variable, the change is visible in the closure. Consider this case:
let text = 'Goodbye' setTimeout(() => console.log(text), 10000) text = 'Hello'
In ten seconds, the string 'Hello'
is printed, even though text
contained 'Goodbye'
when the closure was created.
The lambda expressions and inner classes in Java can also capture variables from enclosing scopes. But in Java, a captured local variable must be effectively final
—that is, its value can never change.
Capturing mutable variables complicates the implementation of closures in JavaScript. A JavaScript closure remembers not just the initial value but the location of the captured variable. And the captured variable is kept alive for as long as the closure exists—even if it is a local variable of a terminated method.
The fundamental idea of a closure is very simple: A free variable inside a function means exactly what it means outside. However, the consequences are profound. It is very useful to capture variables and have them accessible indefinitely. The next section provides a dramatic illustration, by implementing objects and methods entirely with closures.
Let’s say we want to implement bank account objects. Each bank account has a balance. We can deposit and withdraw money.
We want to keep the object state private, so that nobody can modify it except through methods that we provide. Here is an outline of a factory function:
const createAccount = () => { . . . return { deposit: amount => { . . . }, withdraw: amount => { . . . }, getBalance: () => . . . } }
Then we can construct as many accounts as we like:
const harrysAccount = createAccount() const sallysAccount = createAccount() sallysAccount.deposit(500)
Note that an account object contains only methods, not data. After all, if we added the balance to the account object, anyone could modify it. There are no “private” properties in JavaScript.
Where do we store the data? It’s simple—as local variables in the factory function:
const createAccount = () => { let balance = 0 return { . . . } }
We capture the local data in the methods:
const createAccount = () => { . . . return { deposit: amount => { balance += amount }, withdraw: amount => { if (balance >= amount) balance -= amount }, getBalance: () => balance } }
Each account has its own captured balance
variable, namely the one that was created when the factory function was called.
You can provide parameters in the factory function:
const createAccount = (initialBalance) => {
let balance = initialBalance + 10 // Bonus for opening the account
return {
. . .
}
}
You can even capture the parameter variable instead of a local variable:
const createAccount = (balance) => {
balance += 10 // Bonus for opening the account
return {
deposit: amount => {
balance += amount
},
. . .
}
}
At first glance, this looks like an odd way of producing objects. But these objects have two significant advantages. The state, consisting solely of captured local variables of the factory function, is automatically encapsulated. And you avoid the this
parameter, which, as you will see in Chapter 4, is not straightforward in JavaScript.
This technique is sometimes called the “closure pattern” or “factory class pattern,” but I like the term that Douglas Crockford uses in his book How JavaScript Works. He calls them “hard objects.”
To further harden the object, you can use the Object.freeze
method that yields an object whose properties cannot be modified or removed, and to which no new properties can be added.
const createAccount = (balance) => {
return Object.freeze({
deposit: amount => {
balance
+= amount
},
. . .
})
}
As you have seen, JavaScript has its share of unusual features, some of which have proven to be poorly suited for large-scale software development. Strict mode outlaws some of these features. You should always use strict mode.
To enable strict mode, place the line
'use strict'
as the first non-comment line in your file. (Double quotes instead of single quotes are OK, as is a semicolon.)
If you want to force strict mode in the Node.js REPL, start it with
node --use-strict
In a browser console, you need to prefix each line that you want to execute in strict mode with 'use strict';
or 'use strict'
followed by Shift+Enter. That is not very convenient.
You can apply strict mode to individual functions:
function strictInASeaOfSloppy() { 'use strict' . . . }
There is no good reason to use per-function strict mode with modern code. Apply strict mode to the entire file.
Finally, strict mode is enabled inside classes (see Chapter 4) and ECMAScript modules (see Chapter 10).
For the record, here are the key features of strict mode:
Assigning a value to a previously undeclared variable is an error and does not create a global variable. You must use let
, const
, or var
for all variable declarations.
You cannot assign a new value to a read-only global property such as NaN
or undefined
. (Sadly, you can still declare local variables that shadow them.)
Functions can only be declared at the top level of a script or function, not in a nested block.
The delete
operator cannot be applied to “unqualified identifiers.” For example, delete parseInt
is a syntax error. Trying to delete
a property that is not “configurable” (such as delete 'Hello'.length
) causes a runtime error.
You cannot have duplicate function parameters (function average(x, x)
). Of course, you never wanted those, but they are legal in the “sloppy” (non-strict) mode.
You cannot use octal literals with a 0
prefix: 010
is a syntax error, not an octal 10 (which is 8 in decimal). If you want octal, use 0o10
.
The with
statement (which is not discussed in this book) is prohibited.
In strict mode, reading the value of an undeclared variable throws a ReferenceError
. If you need to find out whether a variable has been declared (and initialized), you can’t check
possiblyUndefinedVariable !== undefined
Instead, use the condition
typeof possiblyUndefinedVariable !== 'undefined'
In JavaScript, you do not specify the types of function arguments. Therefore, you can allow callers to supply an argument of one type or another, and handle that argument according to its actual type.
As a somewhat contrived example, the average
function may accept either numbers or arrays.
const average = (x, y) => { let sum = 0 let n = 0 if (Array.isArray(x)) { for (const value of x) { sum += value; n++ } } else { sum = x; n = 1 } if (Array.isArray(y)) { for (const value of y) { sum += value } } else { sum += y; n++ } return n === 0 ? 0 : sum / n }
Now you can call:
result = average(1, 2) result = average([1, 2, 3], 4) result = average(1, [2, 3, 4]) result = average([1, 2], [3, 4, 5])
Table 3-1 shows how to test whether an argument x
conforms to a given type.
Table 3-1 Type Tests
Type |
Test |
Notes |
---|---|---|
String |
typeof x === 'string' || x instanceof String |
|
Regular expression |
|
|
Number |
typeof x === 'number' || x instanceof Number |
|
Anything that can be converted to a number |
|
Obtain the numeric value as |
Array |
|
|
Function |
|
|
Some programmers write functions that turn any argument values into numbers, such as
const average = (x, y) => { return (+x + +y) / 2 }
Then one can call
average('3', [4])
Is that degree of flexibility useful, harmless, or a harbinger of trouble? I don’t recommend it.
Suppose a function is declared with a particular number of parameters, for example:
const average = (x, y) => (x + y) / 2
It appears as if you must supply two arguments when you call the function. However, that is not the JavaScript way. You can call the function with more arguments—they are silently ignored:
let result = average(3, 4, 5) // 3.5
—the last argument is ignored
Conversely, if you supply fewer arguments, then the missing ones are set to undefined
. For example, average(3)
is (3 + undefined) / 2
, or NaN
. If you want to support that call with a meaningful result, you can:
const average = (x, y) => y === undefined ? x : (x + y) / 2
In the preceding section, you saw how to implement a function that is called with fewer arguments than parameters. Instead of manually checking for undefined
argument values, you can provide default arguments in the function declaration. After the parameter, put an =
and an expression for the default—that is, the value that should be used if no argument was passed.
Here is another way of making the average
function work with one argument:
const average = (x, y = x) => (x + y) / 2
If you call average(3)
, then y
is set to x
—that is, 3
—and the correct return value is computed.
You can provide multiple default values:
const average = (x = 0, y = x) => (x + y) / 2
Now average()
returns zero.
You can even provide a default for the first parameter and not the others:
const average = (x = 0, y) => y === undefined ? x : (x + y) / 2
If no argument (or an explicit undefined
) is supplied, the parameter is set to the default or, if none is provided, to undefined
:
average(3) // average(3, undefined) average() // average(0, undefined) average(undefined, 3) // average(0, 3)
As you have seen, you can call a JavaScript function with any number of arguments. To process them all, declare the last parameter of the function as a “rest” parameter by prefixing it with the ...
token:
const average = (first = 0, ...following) => { let sum = first for (const value of following) { sum += value } return sum / (1 + following.length) }
When the function is called, the following
parameter is an array that holds all arguments that have not been used to initialize the preceding parameters. For example, consider the call:
average(1, 7, 2, 9)
Then first
is 1
and following
is the array [7, 2, 9]
.
Many functions and methods accept variable arguments. For example, the Math.max
method yields the largest of its arguments, no matter how many:
let result = Math.max(3, 1, 4, 1, 5, 9, 2, 6) // Sets result
to 9
What if the values are already in an array?
let numbers = [1, 7, 2, 9]
result = Math.max(numbers) // Yields NaN
That doesn’t work. The Math.max
method receives an array with one element—the array [1, 7, 2, 9]
.
Instead, use the “spread” operator—the ...
token placed before an array argument:
result = Math.max(...numbers) // Yields 9
The spread operator spreads out the elements as if they had been provided separately in the call.
Even though the spread operator and rest declaration look the same, their actions are the exact opposites of each other.
First, note that the spread operator is used with an argument, and the rest syntax applies to a variable declaration.
Math.max(...numbers) // Spread operator—argument in function call const max = (...values) => { /* body */} // Rest declaration of parameter variable
The spread operator turns an array (or, in fact, any iterable) into a sequence of values. The rest declaration causes a sequence of values to be placed into an array.
Note that you can use the spread operator even if the function that you call doesn’t have any rest parameters. For example, consider the average
function of the preceding section that has two parameters. If you call
result = average(...numbers)
then all elements of numbers
are passed as arguments to the function. The function uses the first two arguments and ignores the others.
You can also use the spread operator in an array initializer:
let moreNumbers = [1, 2, 3, ...numbers] // Spread operator
Don’t confuse this with the rest declaration used with destructuring. The rest declaration applies to a variable:
let [first, ...following] = numbers // Rest declaration
Since strings are iterable, you can use the spread operator with a string:
let greeting = 'Hello ' let characters = [...greeting]
The characters
array contains the strings 'H'
, 'e'
, 'l'
, 'l'
, 'o'
, ' '
, and ''
.
The syntax for default arguments and rest parameters are equally applicable to the function
syntax:
function average(first = 0, ...following) { . . . }
JavaScript has no “named argument” feature where you provide the parameter names in the call. But you can easily simulate named arguments by passing an object literal:
const result = mkString(values, { leftDelimiter: '(', rightDelimiter: ')' })
That is easy enough for the caller of the function. Now, let’s turn to the function implementation. You can look up the object properties and supply defaults for missing values.
const mkString = (array, config) => { let separator = config.separator === undefined ? ',' : config.separator . . . }
However, that is tedious. It is easier to use destructured parameters with defaults. (See Chapter 1 for the destructuring syntax.)
const mkString = (array, { separator = ',', leftDelimiter = '[', rightDelimiter = ']' }) => { . . . }
The destructuring syntax { separator = ',', leftDelimiter = '[', rightDelimiter = ']' }
declares three parameter variables separator
, leftDelimiter
, and rightDelimiter
that are initialized from the properties with the same names. The defaults are used if the properties are absent or have undefined
values.
It is a good idea to provide a default {}
for the configuration object:
const mkString = (array, { separator = ',', leftDelimiter = '[', rightDelimiter = ']' } = {}) => { . . . }
Now the function can be called without any configuration object:
const result = mkString(values) // The second argument defaults to {}
In this “mad hatter” section, we take up another complex subject that you can easily avoid by following three simple rules. They are:
Don’t use var
Use strict mode
Declare variables and functions before using them
If you want to understand what happens when you don’t follow these rules, read on.
JavaScript has an unusual mechanism for determining the scope of a variable—that is, is the region of a program where the variable can be accessed. Consider a local variable, declared inside a function. In programming languages such as Java, C#, or C++, the scope extends from the point where the variable is declared until the end of the enclosing block. In JavaScript, a local variable declared with let
appears to have the same behavior:
function doStuff() { // Start of block . . . // Attempting to accesssomeVariable
throws aReferenceError
let someVariable // Scope starts here . . . // Can accesssomeVariable
, value isundefined
someVariable = 42 . . . // Can accesssomeVariable
, value is42
} // End of block, scope ends here
However, it is not quite so simple. You can access local variables in functions whose declarations precede the variable declaration:
function doStuff() {
function localWork() {
console.log(someVariable) // OK to access variable
. . .
}
let someVariable = 42
localWork() // Prints 42
}
In JavaScript, every declaration is hoisted to the top of its scope. That is, the variable or function is known to exist even before its declaration, and space is reserved to hold its value.
Inside a nested function, you can reference hoisted variables or functions. Consider the localWork
function in the preceding example. The function knows the location of someVariable
because it is hoisted to the top of the body of doStuff
, even though that variable is declared after the function.
Of course, it can then happen that you access a variable before executing the statement that declares it. With let
and const
declarations, accessing a variable before it is declared throws a ReferenceError
. The variable is in the “temporal dead zone” until its declaration is executed.
However, if a variable is declared with the archaic var
keyword, then its value is simply undefined
until the variable is initialized.
Do not use var
. It declares variables whose scope is the entire function, not the enclosing block. That is too broad:
function someFunction(arr) { //i
,element
already in scope but undefined for (var i = 0; i < arr.length; i++) { var element = arr[i] . . . } //i
,element
still in scope }
Moreover, var
doesn’t play well with closures—see Exercise 10.
Since functions are hoisted, you can call a function before it is declared. In particularly, you can declare mutually recursive functions:
function isEven(n) { return n === 0 ? true : isOdd(n -1) } function isOdd(n) { return n === 0 ? false : isEven(n -1) }
In strict mode, named functions can only be declared at the top level of a script or function, not inside a nested block. In non-strict mode, nested named functions are hoisted to the top of their enclosing function. Exercise 12 shows why this is a bad idea.
As long as you use strict mode and avoid var
declarations, the hoisting behavior is unlikely to result in programming errors. However, it is a good idea to structure your code so that you declare variables and functions before they are used.
In ancient times, JavaScript programmers used “immediately invoked functions” to limit the scope of var
declarations and functions:
(function () { var someVariable = 42 function someFunction(. . .) { . . . } . . . })() // Function is called here—note the()
//someVariable
,someFunction
no longer in scope
After the anonymous function is called, it is never used again. The sole purpose is to encapsulate the declarations.
This device is no longer necessary. Simply use:
{ let someVariable = 42 const someFunction = (. . .) => { . . . } . . . }
The declarations are confined to the block.
If a function is unable to compute a result, it can throw an exception. Depending on the kind of failure, this can be a better strategy than returning an error value such as NaN
or undefined
.
Use a throw
statement to throw an exception:
throw value
The exception value can be a value of any type, but it is conventional to throw an error object. The Error
function produces such an object with a given string describing the reason.
let reason = `Element ${elem} not found` throw Error(reason)
When the throw
statement executes, the function is terminated immediately. No return value is produced, not even undefined
. Execution does not continue in the function call but instead in the nearest catch
or finally
clause, as described in the following sections.
Exception handling is a good mechanism for unpredictable situations that the caller might not be able to handle. It is not so suitable for situations where failure is expected. Consider parsing user input. It is exceedingly likely that some users provide unsuitable input. In JavaScript, it is easy to return a “bottom” value such as undefined
, null
, or NaN
(provided, of course, those could not be valid inputs). Or you can return an object that describes success or failure. For example, in Chapter 9, you will see a method that yields objects of the form { status: 'fulfilled', value: result }
or { status: 'rejected', reason: exception }
.
To catch an exception, use a try
statement. In Chapter 2, you saw how to catch an exception if you are not interested in the exception value. If you want to examine the exception value, add a variable to the catch
clause:
try { // Do work . . . } catch (e) { // Handle exceptions . . . }
The variable in the catch
clause (here, e
) contains the exception value. As you saw in the preceding section, an exception value is conventionally an error object. Such an object has two properties: name
and message
. For example, if you call
JSON.parse('{ age: 42 }')
an exception is thrown with the name 'SyntaxError'
and message 'Unexpected token a in JSON at position 2'
. (The string in this example is invalid JSON because the age
key is not enclosed in double quotes.)
The name of an object produced with the Error
function is 'Error'
. The JavaScript virtual machine throws errors with names 'SyntaxError'
, 'TypeError'
, 'RangeError'
, 'ReferenceError'
, 'URIError'
, or 'InternalError'
.
In the handler, you can record that information in a suitable place. However, in JavaScript it is not usually productive to analyze the error object in detail, as you might in languages such as Java or C++.
When you log an error object on the console, JavaScript execution environments typically display the stack trace—the function and method calls between the throw and catch points. Unfortunately, there is no standard way of accessing the stack trace for logging it elsewhere.
In Java and C++, you can catch exceptions by their type. Then you can handle errors of certain types at a low level and others at a higher level. Such strategies are not easily implemented in JavaScript. A catch
clause catches all exceptions, and the exception objects carry limited information. In JavaScript, exception handlers typically carry out generic recovery or cleanup, without trying to analyze the cause of failure.
When the catch
clause is entered, the exception is deemed to be handled. Processing resumes normally, executing the statements in the catch
clause. The catch
clause can exit with a return
or break
statement, or it can be completed by executing its last statement. In that case, execution moves to the next statement after the catch
clause.
If you log exceptions at one level of your code but deal with failure at a higher level, then you want to rethrow the exception after logging it:
try { // Do work . . . } catch (e) { console.log(e) throw e // Rethrow to a handler that deals with the failure }
finally
ClauseA try
statement can optionally have a finally
clause. The code in the finally
clause executes whether or not an exception occurred.
Let us first look at the simplest case: a try
statement with a finally
clause but no catch
clause:
try { // Acquire resources . . . // Do work . . . } finally { // Relinquish resources . . . }
The finally
clause is executed in all of the following cases:
If all statements in the try
clause completed without throwing an exception
If a return
or break
statement was executed in the try
clause
If an exception occurred in any of the statements of the try
clause
You can also have a try
statement with catch
and finally
clauses:
try { . . . } catch (e) { . . . } finally { . . . }
Now there is an additional pathway. If an exception occurs in the try
clause, the catch
clause is executed. No matter how the catch
clause exits (normally or through a return
/break
/throw
), the finally
clause is executed afterwards.
The purpose of the finally
clause is to have a single location for relinquishing resources (such as file handles or database connections) that were acquired in the try
clause, whether or not an exception occurred.
It is legal, but confusing, to have return
/break
/throw
statements in the finally
clause. These statements take precedence over any statements in the try
and catch
clauses. For example:
try {
// Do work
. . .
return true
} finally {
. . .
return false
}
If the try
block is successful and return true
is executed, the finally
clause follows. Its return false
masks the prior return
statement.
What does the indexOf
function of Section 3.1, “Declaring Functions” (page 51), do when an object is passed instead of an array?
Rewrite the indexOf
function of Section 3.1, “Declaring Functions” (page 51), so that it has a single return at the end.
Write a function values(f, low, high)
that yields an array of function values [f(low), f(low + 1), . . ., f(high)]
.
The sort
method for arrays can take an argument that is a comparison function with two parameters—say, x
and y
. The function returns a negative integer if x
should come before y
, zero if x
and y
are indistinguishable, and a positive integer if x
should come after y
. Write calls, using arrow functions, that sort:
An array of positive integers by decreasing order
An array of people by increasing age
An array of strings by increasing length
Using the “hard objects” technique of Section 3.7, “Hard Objects” (page 59), implement a constructCounter
method that produces counter objects whose count
method increments a counter and yields the new value. The initial value and an optional increment are passed as parameters. (The default increment is 1.)
const myFirstCounter = constructCounter(0, 2) console.log(myFirstCounter.count()) // 0 console.log(myFirstCounter.count()) // 2
A programmer thinks that “named parameters are almost implemented in JavaScript, but order still has precedence,” offering the following “evidence” in the browser console:
function f(a=1, b=2){ console.log(`a=${a}, b=${b}`) }
f() // a=1, b=2
f(a=5) // a=5, b=2
f(a=7, b=10) // a=7, b=10
f(b=10, a=7) // Order is required: a=10, b=7
What is actually going on? (Hint: It has nothing to do with named parameters. Try it in strict mode.)
Write a function average
that computes the average of an arbitrary sequence of numbers, using a rest parameter.
What happens when you pass a string argument to a rest parameter ...str
? Come up with a useful example to take advantage of your observation.
Complete the mkString
function of Section 3.13, “Simulating Named Arguments with Destructuring” (page 66).
The archaic var
keyword interacts poorly with closures. Consider this example:
for (var i = 0; i < 10; i++) { setTimeout(() => console.log(i), 1000 * i) }
What does this code snippet print? Why? (Hint: What is the scope of the variable i
?) What simple change can you make to the code to print the numbers 0, 1, 2, . . . , 9 instead?
Consider this declaration of the factorial function:
const fac = n => n > 1 ? n * fac(n - 1) : 1
Explain why this only works because of variable hoisting.
In sloppy (non-strict) mode, functions can be declared inside a nested block, and they are hoisted to the enclosing function or script. Try out the following example a few times:
if (Math.random() < 0.5) { say('Hello') function say(greeting) { console.log(`${greeting}!`) } } say('Goodbye')
Depending on the result of Math.random
, what is the outcome? What is the scope of say
? When is it initialized? What happens when you activate strict mode?
Implement an average
function that throws an exception if any of its arguments is not a number.
Some programmers are confused by statements that contain all three of try
/catch
/finally
because there are so many possible pathways of control. Show how you can always rewrite such a statement using a try
/catch
statement and a try
/finally
statement.
3.137.157.45