提交 5c13d167 编写于 作者: G gdut-yy

二、三级标题 ch13--ch17

上级 3694893a
...@@ -5,10 +5,10 @@ This kind of asynchronous programming is commonplace in JavaScript, and this cha ...@@ -5,10 +5,10 @@ This kind of asynchronous programming is commonplace in JavaScript, and this cha
Ironically, even though JavaScript provides these powerful features for working with asynchronous code, there are no features of the core language that are themselves asynchronous. In order to demonstrate Promises, async, await, and for/await, therefore, we will first take a detour into client-side and server-side JavaScript to explain some of the asynchronous features of web browsers and Node. (You can learn more about client-side and server-side JavaScript in Chapters 15 and 16.) Ironically, even though JavaScript provides these powerful features for working with asynchronous code, there are no features of the core language that are themselves asynchronous. In order to demonstrate Promises, async, await, and for/await, therefore, we will first take a detour into client-side and server-side JavaScript to explain some of the asynchronous features of web browsers and Node. (You can learn more about client-side and server-side JavaScript in Chapters 15 and 16.)
13.1 Asynchronous Programming with Callbacks ## 13.1 Asynchronous Programming with Callbacks
At its most fundamental level, asynchronous programming in JavaScript is done with callbacks. A callback is a function that you write and then pass to some other function. That other function then invokes (“calls back”) your function when some condition is met or some (asynchronous) event occurs. The invocation of the callback function you provide notifies you of the condition or event, and sometimes, the invocation will include function arguments that provide additional details. This is easier to understand with some concrete examples, and the subsections that follow demonstrate various forms of callback-based asynchronous programming using both client-side JavaScript and Node. At its most fundamental level, asynchronous programming in JavaScript is done with callbacks. A callback is a function that you write and then pass to some other function. That other function then invokes (“calls back”) your function when some condition is met or some (asynchronous) event occurs. The invocation of the callback function you provide notifies you of the condition or event, and sometimes, the invocation will include function arguments that provide additional details. This is easier to understand with some concrete examples, and the subsections that follow demonstrate various forms of callback-based asynchronous programming using both client-side JavaScript and Node.
13.1.1 Timers ### 13.1.1 Timers
One of the simplest kinds of asynchrony is when you want to run some code after a certain amount of time has elapsed. As we saw in §11.10, you can do this with the setTimeout() function: One of the simplest kinds of asynchrony is when you want to run some code after a certain amount of time has elapsed. As we saw in §11.10, you can do this with the setTimeout() function:
setTimeout(checkForUpdates, 60000); setTimeout(checkForUpdates, 60000);
...@@ -25,7 +25,7 @@ let updateIntervalId = setInterval(checkForUpdates, 60000); ...@@ -25,7 +25,7 @@ let updateIntervalId = setInterval(checkForUpdates, 60000);
function stopCheckingForUpdates() { function stopCheckingForUpdates() {
clearInterval(updateIntervalId); clearInterval(updateIntervalId);
} }
13.1.2 Events ### 13.1.2 Events
Client-side JavaScript programs are almost universally event driven: rather than running some kind of predetermined computation, they typically wait for the user to do something and then respond to the user’s actions. The web browser generates an event when the user presses a key on the keyboard, moves the mouse, clicks a mouse button, or touches a touchscreen device. Event-driven JavaScript programs register callback functions for specified types of events in specified contexts, and the web browser invokes those functions whenever the specified events occur. These callback functions are called event handlers or event listeners, and they are registered with addEventListener(): Client-side JavaScript programs are almost universally event driven: rather than running some kind of predetermined computation, they typically wait for the user to do something and then respond to the user’s actions. The web browser generates an event when the user presses a key on the keyboard, moves the mouse, clicks a mouse button, or touches a touchscreen device. Event-driven JavaScript programs register callback functions for specified types of events in specified contexts, and the web browser invokes those functions whenever the specified events occur. These callback functions are called event handlers or event listeners, and they are registered with addEventListener():
// Ask the web browser to return an object representing the HTML // Ask the web browser to return an object representing the HTML
...@@ -37,7 +37,7 @@ let okay = document.querySelector('#confirmUpdateDialog button.okay'); ...@@ -37,7 +37,7 @@ let okay = document.querySelector('#confirmUpdateDialog button.okay');
okay.addEventListener('click', applyUpdate); okay.addEventListener('click', applyUpdate);
In this example, applyUpdate() is a hypothetical callback function that we assume is implemented somewhere else. The call to document.querySelector() returns an object that represents a single specified element in the web page. We call addEventListener() on that element to register our callback. Then the first argument to addEventListener() is a string that specifies the kind of event we’re interested in—a mouse click or touchscreen tap, in this case. If the user clicks or taps on that specific element of the web page, then the browser will invoke our applyUpdate() callback function, passing an object that includes details (such as the time and the mouse pointer coordinates) about the event. In this example, applyUpdate() is a hypothetical callback function that we assume is implemented somewhere else. The call to document.querySelector() returns an object that represents a single specified element in the web page. We call addEventListener() on that element to register our callback. Then the first argument to addEventListener() is a string that specifies the kind of event we’re interested in—a mouse click or touchscreen tap, in this case. If the user clicks or taps on that specific element of the web page, then the browser will invoke our applyUpdate() callback function, passing an object that includes details (such as the time and the mouse pointer coordinates) about the event.
13.1.3 Network Events ### 13.1.3 Network Events
Another common source of asynchrony in JavaScript programming is network requests. JavaScript running in the browser can fetch data from a web server with code like this: Another common source of asynchrony in JavaScript programming is network requests. JavaScript running in the browser can fetch data from a web server with code like this:
function getCurrentVersionNumber(versionCallback) { // Note callback argument function getCurrentVersionNumber(versionCallback) { // Note callback argument
...@@ -68,7 +68,7 @@ Notice that the code example above does not call addEventListener() as our previ ...@@ -68,7 +68,7 @@ Notice that the code example above does not call addEventListener() as our previ
Another thing to note about the getCurrentVersionNumber() function in this example code is that, because it makes an asynchronous request, it cannot synchronously return the value (the current version number) that the caller is interested in. Instead, the caller passes a callback function, which is invoked when the result is ready or when an error occurs. In this case, the caller supplies a callback function that expects two arguments. If the XMLHttpRequest works correctly, then getCurrentVersionNumber() invokes the callback with a null first argument and the version number as the second argument. Or, if an error occurs, then getCurrentVersionNumber() invokes the callback with error details in the first argument and null as the second argument. Another thing to note about the getCurrentVersionNumber() function in this example code is that, because it makes an asynchronous request, it cannot synchronously return the value (the current version number) that the caller is interested in. Instead, the caller passes a callback function, which is invoked when the result is ready or when an error occurs. In this case, the caller supplies a callback function that expects two arguments. If the XMLHttpRequest works correctly, then getCurrentVersionNumber() invokes the callback with a null first argument and the version number as the second argument. Or, if an error occurs, then getCurrentVersionNumber() invokes the callback with error details in the first argument and null as the second argument.
13.1.4 Callbacks and Events in Node ### 13.1.4 Callbacks and Events in Node
The Node.js server-side JavaScript environment is deeply asynchronous and defines many APIs that use callbacks and events. The default API for reading the contents of a file, for example, is asynchronous and invokes a callback function when the contents of the file have been read: The Node.js server-side JavaScript environment is deeply asynchronous and defines many APIs that use callbacks and events. The default API for reading the contents of a file, for example, is asynchronous and invokes a callback function when the contents of the file have been read:
const fs = require("fs"); // The "fs" module has filesystem-related APIs const fs = require("fs"); // The "fs" module has filesystem-related APIs
...@@ -128,7 +128,7 @@ function getText(url, callback) { ...@@ -128,7 +128,7 @@ function getText(url, callback) {
callback(err, null); callback(err, null);
}); });
} }
13.2 Promises ## 13.2 Promises
Now that we’ve seen examples of callback and event-based asynchronous programming in client-side and server-side JavaScript environments, we can introduce Promises, a core language feature designed to simplify asynchronous programming. Now that we’ve seen examples of callback and event-based asynchronous programming in client-side and server-side JavaScript environments, we can introduce Promises, a core language feature designed to simplify asynchronous programming.
A Promise is an object that represents the result of an asynchronous computation. That result may or may not be ready yet, and the Promise API is intentionally vague about this: there is no way to synchronously get the value of a Promise; you can only ask the Promise to call a callback function when the value is ready. If you are defining an asynchronous API like the getText() function in the previous section, but want to make it Promise-based, omit the callback argument, and instead return a Promise object. The caller can then register one or more callbacks on this Promise object, and they will be invoked when the asynchronous computation is done. A Promise is an object that represents the result of an asynchronous computation. That result may or may not be ready yet, and the Promise API is intentionally vague about this: there is no way to synchronously get the value of a Promise; you can only ask the Promise to call a callback function when the value is ready. If you are defining an asynchronous API like the getText() function in the previous section, but want to make it Promise-based, omit the callback argument, and instead return a Promise object. The caller can then register one or more callbacks on this Promise object, and they will be invoked when the asynchronous computation is done.
...@@ -150,7 +150,7 @@ Demonstrate how to create your own Promise-based APIs ...@@ -150,7 +150,7 @@ Demonstrate how to create your own Promise-based APIs
IMPORTANT IMPORTANT
Promises seem simple at first, and the basic use case for Promises is, in fact, straightforward and simple. But they can become surprisingly confusing for anything beyond the simplest use cases. Promises are a powerful idiom for asynchronous programming, but you need to understand them deeply to use them correctly and confidently. It is worth taking the time to develop that deep understanding, however, and I urge you to study this long chapter carefully. Promises seem simple at first, and the basic use case for Promises is, in fact, straightforward and simple. But they can become surprisingly confusing for anything beyond the simplest use cases. Promises are a powerful idiom for asynchronous programming, but you need to understand them deeply to use them correctly and confidently. It is worth taking the time to develop that deep understanding, however, and I urge you to study this long chapter carefully.
13.2.1 Using Promises ### 13.2.1 Using Promises
With the advent of Promises in the core JavaScript language, web browsers have begun to implement Promise-based APIs. In the previous section, we implemented a getText() function that made an asynchronous HTTP request and passed the body of the HTTP response to a specified callback function as a string. Imagine a variant of this function, getJSON(), which parses the body of the HTTP response as JSON and returns a Promise instead of accepting a callback argument. We will implement a getJSON() function later in this chapter, but for now, let’s look at how we would use this Promise-returning utility function: With the advent of Promises in the core JavaScript language, web browsers have begun to implement Promise-based APIs. In the previous section, we implemented a getText() function that made an asynchronous HTTP request and passed the body of the HTTP response to a specified callback function as a string. Imagine a variant of this function, getJSON(), which parses the body of the HTTP response as JSON and returns a Promise instead of accepting a callback argument. We will implement a getJSON() function later in this chapter, but for now, let’s look at how we would use this Promise-returning utility function:
getJSON(url).then(jsonData => { getJSON(url).then(jsonData => {
...@@ -201,7 +201,7 @@ Remember how we defined Promises at the start of this section: “a Promise is a ...@@ -201,7 +201,7 @@ Remember how we defined Promises at the start of this section: “a Promise is a
The reason that I want to be precise about Promise terminology is that Promises can also be resolved. It is easy to confuse this resolved state with the fulfilled state or with settled state, but it is not precisely the same as either. Understanding the resolved state is one of the keys to a deep understanding of Promises, and I’ll come back to it after we’ve discussed Promise chains below. The reason that I want to be precise about Promise terminology is that Promises can also be resolved. It is easy to confuse this resolved state with the fulfilled state or with settled state, but it is not precisely the same as either. Understanding the resolved state is one of the keys to a deep understanding of Promises, and I’ll come back to it after we’ve discussed Promise chains below.
13.2.2 Chaining Promises ### 13.2.2 Chaining Promises
One of the most important benefits of Promises is that they provide a natural way to express a sequence of asynchronous operations as a linear chain of then() method invocations, without having to nest each operation within the callback of the previous one. Here, for example, is a hypothetical Promise chain: One of the most important benefits of Promises is that they provide a natural way to express a sequence of asynchronous operations as a linear chain of then() method invocations, without having to nest each operation within the callback of the previous one. Here, for example, is a hypothetical Promise chain:
fetch(documentURL) // Make an HTTP request fetch(documentURL) // Make an HTTP request
...@@ -272,7 +272,7 @@ Let’s assume that task 2 completes normally and is able to parse the body of t ...@@ -272,7 +272,7 @@ Let’s assume that task 2 completes normally and is able to parse the body of t
The value that fulfills promise 2 becomes the input to task 3 when it is passed to the callback2() function. This third task now displays the data to the user in some unspecified way. When task 3 is complete (assuming it completes normally), then promise 3 will be fulfilled. But because we never did anything with promise 3, nothing happens when that Promise settles, and the chain of asynchronous computation ends at this point. The value that fulfills promise 2 becomes the input to task 3 when it is passed to the callback2() function. This third task now displays the data to the user in some unspecified way. When task 3 is complete (assuming it completes normally), then promise 3 will be fulfilled. But because we never did anything with promise 3, nothing happens when that Promise settles, and the chain of asynchronous computation ends at this point.
13.2.3 Resolving Promises ### 13.2.3 Resolving Promises
While explaining the URL-fetching Promise chain with the list in the last section, we talked about promises 1, 2, and 3. But there is actually a fourth Promise object involved as well, and this brings us to our important discussion of what it means for a Promise to be “resolved.” While explaining the URL-fetching Promise chain with the list in the last section, we talked about promises 1, 2, and 3. But there is actually a fourth Promise object involved as well, and this brings us to our important discussion of what it means for a Promise to be “resolved.”
Remember that fetch() returns a Promise object which, when fulfilled, passes a Response object to the callback function we register. This Response object has .text(), .json(), and other methods to request the body of the HTTP response in various forms. But since the body may not yet have arrived, these methods must return Promise objects. In the example we’ve been studying, “task 2” calls the .json() method and returns its value. This is the fourth Promise object, and it is the return value of the callback1() function. Remember that fetch() returns a Promise object which, when fulfilled, passes a Response object to the callback function we register. This Response object has .text(), .json(), and other methods to request the body of the HTTP response in various forms. But since the body may not yet have arrived, these methods must return Promise objects. In the example we’ve been studying, “task 2” calls the .json() method and returns its value. This is the fourth Promise object, and it is the return value of the callback1() function.
...@@ -301,7 +301,7 @@ This can be one of the trickiest parts of JavaScript to understand, and you may ...@@ -301,7 +301,7 @@ This can be one of the trickiest parts of JavaScript to understand, and you may
js7e 1301 js7e 1301
Figure 13-1. Fetching a URL with Promises Figure 13-1. Fetching a URL with Promises
13.2.4 More on Promises and Errors ### 13.2.4 More on Promises and Errors
Earlier in the chapter, we saw that you can pass a second callback function to the .then() method and that this second function will be invoked if the Promise is rejected. When that happens, the argument to this second callback function is a value—typically an Error object—that represents the reason for the rejection. We also learned that it is uncommon (and even unidiomatic) to pass two callbacks to a .then() method. Instead, Promise-related errors are typically handled by adding a .catch() method invocation to a Promise chain. Now that we have examined Promise chains, we can return to error handling and discuss it in more detail. To preface the discussion, I’d like to stress that careful error handling is really important when doing asynchronous programming. With synchronous code, if you leave out error-handling code, you’ll at least get an exception and a stack trace that you can use to figure out what is going wrong. With asynchronous code, unhandled exceptions will often go unreported, and errors can occur silently, making them much harder to debug. The good news is that the .catch() method makes it easy to handle errors when working with Promises. Earlier in the chapter, we saw that you can pass a second callback function to the .then() method and that this second function will be invoked if the Promise is rejected. When that happens, the argument to this second callback function is a value—typically an Error object—that represents the reason for the rejection. We also learned that it is uncommon (and even unidiomatic) to pass two callbacks to a .then() method. Instead, Promise-related errors are typically handled by adding a .catch() method invocation to a Promise chain. Now that we have examined Promise chains, we can return to error handling and discuss it in more detail. To preface the discussion, I’d like to stress that careful error handling is really important when doing asynchronous programming. With synchronous code, if you leave out error-handling code, you’ll at least get an exception and a stack trace that you can use to figure out what is going wrong. With asynchronous code, unhandled exceptions will often go unreported, and errors can occur silently, making them much harder to debug. The good news is that the .catch() method makes it easy to handle errors when working with Promises.
THE CATCH AND FINALLY METHODS THE CATCH AND FINALLY METHODS
...@@ -401,7 +401,7 @@ Recall from Chapter 8 that arrow functions allow a lot of shortcuts. Since there ...@@ -401,7 +401,7 @@ Recall from Chapter 8 that arrow functions allow a lot of shortcuts. Since there
.catch(e => { wait(500).then(queryDatabase) }) .catch(e => { wait(500).then(queryDatabase) })
By adding the curly braces, we no longer get the automatic return. This function now returns undefined instead of returning a Promise, which means that the next stage in this Promise chain will be invoked with undefined as its input rather than the result of the retried query. It is a subtle error that may not be easy to debug. By adding the curly braces, we no longer get the automatic return. This function now returns undefined instead of returning a Promise, which means that the next stage in this Promise chain will be invoked with undefined as its input rather than the result of the retried query. It is a subtle error that may not be easy to debug.
13.2.5 Promises in Parallel ### 13.2.5 Promises in Parallel
We’ve spent a lot of time talking about Promise chains for sequentially running the asynchronous steps of a larger asynchronous operation. Sometimes, though, we want to execute a number of asynchronous operations in parallel. The function Promise.all() can do this. Promise.all() takes an array of Promise objects as its input and returns a Promise. The returned Promise will be rejected if any of the input Promises are rejected. Otherwise, it will be fulfilled with an array of the fulfillment values of each of the input Promises. So, for example, if you want to fetch the text content of multiple URLs, you could use code like this: We’ve spent a lot of time talking about Promise chains for sequentially running the asynchronous steps of a larger asynchronous operation. Sometimes, though, we want to execute a number of asynchronous operations in parallel. The function Promise.all() can do this. Promise.all() takes an array of Promise objects as its input and returns a Promise. The returned Promise will be rejected if any of the input Promises are rejected. Otherwise, it will be fulfilled with an array of the fulfillment values of each of the input Promises. So, for example, if you want to fetch the text content of multiple URLs, you could use code like this:
// We start with an array of URLs // We start with an array of URLs
...@@ -423,7 +423,7 @@ Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => { ...@@ -423,7 +423,7 @@ Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => {
}); });
Occasionally, you may want to run a number of Promises at once but may only care about the value of the first one to fulfill. In that case, you can use Promise.race() instead of Promise.all(). It returns a Promise that is fulfilled or rejected when the first of the Promises in the input array is fulfilled or rejected. (Or, if there are any non-Promise values in the input array, it simply returns the first of those.) Occasionally, you may want to run a number of Promises at once but may only care about the value of the first one to fulfill. In that case, you can use Promise.race() instead of Promise.all(). It returns a Promise that is fulfilled or rejected when the first of the Promises in the input array is fulfilled or rejected. (Or, if there are any non-Promise values in the input array, it simply returns the first of those.)
13.2.6 Making Promises ### 13.2.6 Making Promises
We’ve used the Promise-returning function fetch() in many of the previous examples because it is one of the simplest functions built in to web browsers that returns a Promise. Our discussion of Promises has also relied on hypothetical Promise-returning functions getJSON() and wait(). Functions written to return Promises really are quite useful, and this section shows how you can create your own Promise-based APIs. In particular, we’ll show implementations of getJSON() and wait(). We’ve used the Promise-returning function fetch() in many of the previous examples because it is one of the simplest functions built in to web browsers that returns a Promise. Our discussion of Promises has also relied on hypothetical Promise-returning functions getJSON() and wait(). Functions written to return Promises really are quite useful, and this section shows how you can create your own Promise-based APIs. In particular, we’ll show implementations of getJSON() and wait().
PROMISES BASED ON OTHER PROMISES PROMISES BASED ON OTHER PROMISES
...@@ -513,7 +513,7 @@ function getJSON(url) { ...@@ -513,7 +513,7 @@ function getJSON(url) {
}); });
}); });
} }
13.2.7 Promises in Sequence ### 13.2.7 Promises in Sequence
Promise.all() makes it easy to run an arbitrary number of Promises in parallel. And Promise chains make it easy to express a sequence of a fixed number of Promises. Running an arbitrary number of Promises in sequence is trickier, however. Suppose, for example, that you have an array of URLs to fetch, but that to avoid overloading your network, you want to fetch them one at a time. If the array is of arbitrary length and unknown content, you can’t write out a Promise chain in advance, so you need to build one dynamically, with code like this: Promise.all() makes it easy to run an arbitrary number of Promises in parallel. And Promise chains make it easy to express a sequence of a fixed number of Promises. Running an arbitrary number of Promises in sequence is trickier, however. Suppose, for example, that you have an array of URLs to fetch, but that to avoid overloading your network, you want to fetch them one at a time. If the array is of arbitrary length and unknown content, you can’t write out a Promise chain in advance, so you need to build one dynamically, with code like this:
function fetchSequentially(urls) { function fetchSequentially(urls) {
...@@ -601,19 +601,19 @@ function fetchBody(url) { return fetch(url).then(r => r.text()); } ...@@ -601,19 +601,19 @@ function fetchBody(url) { return fetch(url).then(r => r.text()); }
promiseSequence(urls, fetchBody) promiseSequence(urls, fetchBody)
.then(bodies => { /* do something with the array of strings */ }) .then(bodies => { /* do something with the array of strings */ })
.catch(console.error); .catch(console.error);
13.3 async and await ## 13.3 async and await
ES2017 introduces two new keywords—async and await—that represent a paradigm shift in asynchronous JavaScript programming. These new keywords dramatically simplify the use of Promises and allow us to write Promise-based, asynchronous code that looks like synchronous code that blocks while waiting for network responses or other asynchronous events. Although it is still important to understand how Promises work, much of their complexity (and sometimes even their very presence!) vanishes when you use them with async and await. ES2017 introduces two new keywords—async and await—that represent a paradigm shift in asynchronous JavaScript programming. These new keywords dramatically simplify the use of Promises and allow us to write Promise-based, asynchronous code that looks like synchronous code that blocks while waiting for network responses or other asynchronous events. Although it is still important to understand how Promises work, much of their complexity (and sometimes even their very presence!) vanishes when you use them with async and await.
As we discussed earlier in the chapter, asynchronous code can’t return a value or throw an exception the way that regular synchronous code can. And this is why Promises are designed the way the are. The value of a fulfilled Promise is like the return value of a synchronous function. And the value of a rejected Promise is like a value thrown by a synchronous function. This latter similarity is made explicit by the naming of the .catch() method. async and await take efficient, Promise-based code and hide the Promises so that your asynchronous code can be as easy to read and as easy to reason about as inefficient, blocking, synchronous code. As we discussed earlier in the chapter, asynchronous code can’t return a value or throw an exception the way that regular synchronous code can. And this is why Promises are designed the way the are. The value of a fulfilled Promise is like the return value of a synchronous function. And the value of a rejected Promise is like a value thrown by a synchronous function. This latter similarity is made explicit by the naming of the .catch() method. async and await take efficient, Promise-based code and hide the Promises so that your asynchronous code can be as easy to read and as easy to reason about as inefficient, blocking, synchronous code.
13.3.1 await Expressions ### 13.3.1 await Expressions
The await keyword takes a Promise and turns it back into a return value or a thrown exception. Given a Promise object p, the expression await p waits until p settles. If p fulfills, then the value of await p is the fulfillment value of p. On the other hand, if p is rejected, then the await p expression throws the rejection value of p. We don’t usually use await with a variable that holds a Promise; instead, we use it before the invocation of a function that returns a Promise: The await keyword takes a Promise and turns it back into a return value or a thrown exception. Given a Promise object p, the expression await p waits until p settles. If p fulfills, then the value of await p is the fulfillment value of p. On the other hand, if p is rejected, then the await p expression throws the rejection value of p. We don’t usually use await with a variable that holds a Promise; instead, we use it before the invocation of a function that returns a Promise:
let response = await fetch("/api/user/profile"); let response = await fetch("/api/user/profile");
let profile = await response.json(); let profile = await response.json();
It is critical to understand right away that the await keyword does not cause your program to block and literally do nothing until the specified Promise settles. The code remains asynchronous, and the await simply disguises this fact. This means that any code that uses await is itself asynchronous. It is critical to understand right away that the await keyword does not cause your program to block and literally do nothing until the specified Promise settles. The code remains asynchronous, and the await simply disguises this fact. This means that any code that uses await is itself asynchronous.
13.3.2 async Functions ### 13.3.2 async Functions
Because any code that uses await is asynchronous, there is one critical rule: you can only use the await keyword within functions that have been declared with the async keyword. Here’s a version of the getHighScore() function from earlier in the chapter, rewritten to use async and await: Because any code that uses await is asynchronous, there is one critical rule: you can only use the await keyword within functions that have been declared with the async keyword. Here’s a version of the getHighScore() function from earlier in the chapter, rewritten to use async and await:
async function getHighScore() { async function getHighScore() {
...@@ -631,7 +631,7 @@ But remember, that line of code will only work if it is inside another async fun ...@@ -631,7 +631,7 @@ But remember, that line of code will only work if it is inside another async fun
getHighScore().then(displayHighScore).catch(console.error); getHighScore().then(displayHighScore).catch(console.error);
You can use the async keyword with any kind of function. It works with the function keyword as a statement or as an expression. It works with arrow functions and with the method shortcut form in classes and object literals. (See Chapter 8 for more about the various ways to write functions.) You can use the async keyword with any kind of function. It works with the function keyword as a statement or as an expression. It works with arrow functions and with the method shortcut form in classes and object literals. (See Chapter 8 for more about the various ways to write functions.)
13.3.3 Awaiting Multiple Promises ### 13.3.3 Awaiting Multiple Promises
Suppose that we’ve written our getJSON() function using async: Suppose that we’ve written our getJSON() function using async:
async function getJSON(url) { async function getJSON(url) {
...@@ -646,7 +646,7 @@ let value2 = await getJSON(url2); ...@@ -646,7 +646,7 @@ let value2 = await getJSON(url2);
The problem with this code is that it is unnecessarily sequential: the fetch of the second URL will not begin until the first fetch is complete. If the second URL does not depend on the value obtained from the first URL, then we should probably try to fetch the two values at the same time. This is a case where the Promise-based nature of async functions shows. In order to await a set of concurrently executing async functions, we use Promise.all() just as we would if working with Promises directly: The problem with this code is that it is unnecessarily sequential: the fetch of the second URL will not begin until the first fetch is complete. If the second URL does not depend on the value obtained from the first URL, then we should probably try to fetch the two values at the same time. This is a case where the Promise-based nature of async functions shows. In order to await a set of concurrently executing async functions, we use Promise.all() just as we would if working with Promises directly:
let [value1, value2] = await Promise.all([getJSON(url1), getJSON(url2)]); let [value1, value2] = await Promise.all([getJSON(url1), getJSON(url2)]);
13.3.4 Implementation Details ### 13.3.4 Implementation Details
Finally, in order to understand how async functions work, it may help to think about what is going on under the hood. Finally, in order to understand how async functions work, it may help to think about what is going on under the hood.
Suppose you write an async function like this: Suppose you write an async function like this:
...@@ -666,12 +666,12 @@ function f(x) { ...@@ -666,12 +666,12 @@ function f(x) {
} }
It is harder to express the await keyword in terms of a syntax transformation like this one. But think of the await keyword as a marker that breaks a function body up into separate, synchronous chunks. An ES2017 interpreter can break the function body up into a sequence of separate subfunctions, each of which gets passed to the then() method of the await-marked Promise that precedes it. It is harder to express the await keyword in terms of a syntax transformation like this one. But think of the await keyword as a marker that breaks a function body up into separate, synchronous chunks. An ES2017 interpreter can break the function body up into a sequence of separate subfunctions, each of which gets passed to the then() method of the await-marked Promise that precedes it.
13.4 Asynchronous Iteration ## 13.4 Asynchronous Iteration
We began this chapter with a discussion of callback- and event-based asynchrony, and when we introduced Promises, we noted that they were useful for single-shot asynchronous computations but were not suitable for use with sources of repetitive asynchronous events, such as setInterval(), the “click” event in a web browser, or the “data” event on a Node stream. Because single Promises do not work for sequences of asynchronous events, we also cannot use regular async functions and the await statements for these things. We began this chapter with a discussion of callback- and event-based asynchrony, and when we introduced Promises, we noted that they were useful for single-shot asynchronous computations but were not suitable for use with sources of repetitive asynchronous events, such as setInterval(), the “click” event in a web browser, or the “data” event on a Node stream. Because single Promises do not work for sequences of asynchronous events, we also cannot use regular async functions and the await statements for these things.
ES2018 provides a solution, however. Asynchronous iterators are like the iterators described in Chapter 12, but they are Promise-based and are meant to be used with a new form of the for/of loop: for/await. ES2018 provides a solution, however. Asynchronous iterators are like the iterators described in Chapter 12, but they are Promise-based and are meant to be used with a new form of the for/of loop: for/await.
13.4.1 The for/await Loop ### 13.4.1 The for/await Loop
Node 12 makes its readable streams asynchronously iterable. This means you can read successive chunks of data from a stream with a for/await loop like this one: Node 12 makes its readable streams asynchronously iterable. This means you can read successive chunks of data from a stream with a for/await loop like this one:
const fs = require("fs"); const fs = require("fs");
...@@ -705,7 +705,7 @@ In this case, the for/await loop just builds the await call into the loop and ma ...@@ -705,7 +705,7 @@ In this case, the for/await loop just builds the await call into the loop and ma
It is important to realize, however, that we’re using for/await with a regular iterator in this example. Things are more interesting with fully asynchronous iterators. It is important to realize, however, that we’re using for/await with a regular iterator in this example. Things are more interesting with fully asynchronous iterators.
13.4.2 Asynchronous Iterators ### 13.4.2 Asynchronous Iterators
Let’s review some terminology from Chapter 12. An iterable object is one that can be used with a for/of loop. It defines a method with the symbolic name Symbol.iterator. This method returns an iterator object. The iterator object has a next() method, which can be called repeatedly to obtain the values of the iterable object. The next() method of the iterator object returns iteration result objects. The iteration result object has a value property and/or a done property. Let’s review some terminology from Chapter 12. An iterable object is one that can be used with a for/of loop. It defines a method with the symbolic name Symbol.iterator. This method returns an iterator object. The iterator object has a next() method, which can be called repeatedly to obtain the values of the iterable object. The next() method of the iterator object returns iteration result objects. The iteration result object has a value property and/or a done property.
Asynchronous iterators are quite similar to regular iterators, but there are two important differences. First, an asynchronously iterable object implements a method with the symbolic name Symbol.asyncIterator instead of Symbol.iterator. (As we saw earlier, for/await is compatible with regular iterable objects but it prefers asynchronously iterable objects, and tries the Symbol.asyncIterator method before it tries the Symbol.iterator method.) Second, the next() method of an asynchronous iterator returns a Promise that resolves to an iterator result object instead of returning an iterator result object directly. Asynchronous iterators are quite similar to regular iterators, but there are two important differences. First, an asynchronously iterable object implements a method with the symbolic name Symbol.asyncIterator instead of Symbol.iterator. (As we saw earlier, for/await is compatible with regular iterable objects but it prefers asynchronously iterable objects, and tries the Symbol.asyncIterator method before it tries the Symbol.iterator method.) Second, the next() method of an asynchronous iterator returns a Promise that resolves to an iterator result object instead of returning an iterator result object directly.
...@@ -713,7 +713,7 @@ Asynchronous iterators are quite similar to regular iterators, but there are two ...@@ -713,7 +713,7 @@ Asynchronous iterators are quite similar to regular iterators, but there are two
NOTE NOTE
In the previous section, when we used for/await on a regular, synchronously iterable array of Promises, we were working with synchronous iterator result objects in which the value property was a Promise object but the done property was synchronous. True asynchronous iterators return Promises for iteration result objects, and both the value and the done properties are asynchronous. The difference is a subtle one: with asynchronous iterators, the choice about when iteration ends can be made asynchronously. In the previous section, when we used for/await on a regular, synchronously iterable array of Promises, we were working with synchronous iterator result objects in which the value property was a Promise object but the done property was synchronous. True asynchronous iterators return Promises for iteration result objects, and both the value and the done properties are asynchronous. The difference is a subtle one: with asynchronous iterators, the choice about when iteration ends can be made asynchronously.
13.4.3 Asynchronous Generators ### 13.4.3 Asynchronous Generators
As we saw in Chapter 12, the easiest way to implement an iterator is often to use a generator. The same is true for asynchronous iterators, which we can implement with generator functions that we declare async. An async generator has the features of async functions and the features of generators: you can use await as you would in a regular async function, and you can use yield as you would in a regular generator. But values that you yield are automatically wrapped in Promises. Even the syntax for async generators is a combination: async function and function * combine into async function *. Here is an example that shows how you might use an async generator and a for/await loop to repetitively run code at fixed intervals using loop syntax instead of a setInterval() callback function: As we saw in Chapter 12, the easiest way to implement an iterator is often to use a generator. The same is true for asynchronous iterators, which we can implement with generator functions that we declare async. An async generator has the features of async functions and the features of generators: you can use await as you would in a regular async function, and you can use yield as you would in a regular generator. But values that you yield are automatically wrapped in Promises. Even the syntax for async generators is a combination: async function and function * combine into async function *. Here is an example that shows how you might use an async generator and a for/await loop to repetitively run code at fixed intervals using loop syntax instead of a setInterval() callback function:
// A Promise-based wrapper around setTimeout() that we can use await with. // A Promise-based wrapper around setTimeout() that we can use await with.
...@@ -737,7 +737,7 @@ async function test() { // Async so we can use for/await ...@@ -737,7 +737,7 @@ async function test() { // Async so we can use for/await
console.log(tick); console.log(tick);
} }
} }
13.4.4 Implementing Asynchronous Iterators ### 13.4.4 Implementing Asynchronous Iterators
Instead of using async generators to implement asynchronous iterators, it is also possible to implement them directly by defining an object with a Symbol.asyncIterator() method that returns an object with a next() method that returns a Promise that resolves to an iterator result object. In the following code, we re-implement the clock() function from the preceding example so that it is not a generator and instead just returns an asynchronously iterable object. Notice that the next() method in this example does not explicitly return a Promise; instead, we just declare next() to be async: Instead of using async generators to implement asynchronous iterators, it is also possible to implement them directly by defining an object with a Symbol.asyncIterator() method that returns an object with a next() method that returns a Promise that resolves to an iterator result object. In the following code, we re-implement the clock() function from the preceding example so that it is not a generator and instead just returns an asynchronously iterable object. Notice that the next() method in this example does not explicitly return a Promise; instead, we just declare next() to be async:
function clock(interval, max=Infinity) { function clock(interval, max=Infinity) {
...@@ -868,7 +868,7 @@ async function handleKeys() { ...@@ -868,7 +868,7 @@ async function handleKeys() {
console.log(event.key); console.log(event.key);
} }
} }
13.5 Summary ## 13.5 Summary
In this chapter, you have learned: In this chapter, you have learned:
Most real-world JavaScript programming is asynchronous. Most real-world JavaScript programming is asynchronous.
......
...@@ -19,7 +19,7 @@ The metaprogramming topics covered in this chapter include: ...@@ -19,7 +19,7 @@ The metaprogramming topics covered in this chapter include:
§14.7 Controlling object behavior with Proxy §14.7 Controlling object behavior with Proxy
14.1 Property Attributes ## 14.1 Property Attributes
The properties of a JavaScript object have names and values, of course, but each property also has three associated attributes that specify how that property behaves and what you can do with it: The properties of a JavaScript object have names and values, of course, but each property also has three associated attributes that specify how that property behaves and what you can do with it:
The writable attribute specifies whether or not the value of a property can change. The writable attribute specifies whether or not the value of a property can change.
...@@ -170,7 +170,7 @@ p.count // => 1: This is now just a data property so ...@@ -170,7 +170,7 @@ p.count // => 1: This is now just a data property so
p.count // => 1: ...the counter does not increment. p.count // => 1: ...the counter does not increment.
q.count // => 2: Incremented once when we copied it the first time, q.count // => 2: Incremented once when we copied it the first time,
q.count // => 3: ...but we copied the getter method so it increments. q.count // => 3: ...but we copied the getter method so it increments.
14.2 Object Extensibility ## 14.2 Object Extensibility
The extensible attribute of an object specifies whether new properties can be added to the object or not. Ordinary JavaScript objects are extensible by default, but you can change that with the functions described in this section. The extensible attribute of an object specifies whether new properties can be added to the object or not. Ordinary JavaScript objects are extensible by default, but you can change that with the functions described in this section.
To determine whether an object is extensible, pass it to Object.isExtensible(). To make an object non-extensible, pass it to Object.preventExtensions(). Once you have done this, any attempt to add a new property to the object will throw a TypeError in strict mode and simply fail silently without an error in non-strict mode. In addition, attempting to change the prototype (see §14.3) of a non-extensible object will always throw a TypeError. To determine whether an object is extensible, pass it to Object.isExtensible(). To make an object non-extensible, pass it to Object.preventExtensions(). Once you have done this, any attempt to add a new property to the object will throw a TypeError in strict mode and simply fail silently without an error in non-strict mode. In addition, attempting to change the prototype (see §14.3) of a non-extensible object will always throw a TypeError.
...@@ -194,7 +194,7 @@ let o = Object.seal(Object.create(Object.freeze({x: 1}), ...@@ -194,7 +194,7 @@ let o = Object.seal(Object.create(Object.freeze({x: 1}),
{y: {value: 2, writable: true}})); {y: {value: 2, writable: true}}));
If you are writing a JavaScript library that passes objects to callback functions written by the users of your library, you might use Object.freeze() on those objects to prevent the user’s code from modifying them. This is easy and convenient to do, but there are trade-offs: frozen objects can interfere with common JavaScript testing strategies, for example. If you are writing a JavaScript library that passes objects to callback functions written by the users of your library, you might use Object.freeze() on those objects to prevent the user’s code from modifying them. This is easy and convenient to do, but there are trade-offs: frozen objects can interfere with common JavaScript testing strategies, for example.
14.3 The prototype Attribute ## 14.3 The prototype Attribute
An object’s prototype attribute specifies the object from which it inherits properties. (Review §6.2.3 and §6.3.2 for more on prototypes and property inheritance.) This is such an important attribute that we usually simply say “the prototype of o" rather than “the prototype attribute of o.” Remember also that when prototype appears in code font, it refers to an ordinary object property, not to the prototype attribute: Chapter 9 explained that the prototype property of a constructor function specifies the prototype attribute of the objects created with that constructor. An object’s prototype attribute specifies the object from which it inherits properties. (Review §6.2.3 and §6.3.2 for more on prototypes and property inheritance.) This is such an important attribute that we usually simply say “the prototype of o" rather than “the prototype attribute of o.” Remember also that when prototype appears in code font, it refers to an ordinary object property, not to the prototype attribute: Chapter 9 explained that the prototype property of a constructor function specifies the prototype attribute of the objects created with that constructor.
The prototype attribute is set when an object is created. Objects created from object literals use Object.prototype as their prototype. Objects created with new use the value of the prototype property of their constructor function as their prototype. And objects created with Object.create() use the first argument to that function (which may be null) as their prototype. The prototype attribute is set when an object is created. Objects created from object literals use Object.prototype as their prototype. Objects created with new use the value of the prototype property of their constructor function as their prototype. And objects created with Object.create() use the first argument to that function (which may be null) as their prototype.
...@@ -237,15 +237,15 @@ let o = { ...@@ -237,15 +237,15 @@ let o = {
__proto__: p __proto__: p
}; };
o.z // => 3: o inherits from p o.z // => 3: o inherits from p
14.4 Well-Known Symbols ## 14.4 Well-Known Symbols
The Symbol type was added to JavaScript in ES6, and one of the primary reasons for doing so was to safely add extensions to the language without breaking compatibility with code already deployed on the web. We saw an example of this in Chapter 12, where we learned that you can make a class iterable by implementing a method whose “name” is the Symbol Symbol.iterator. The Symbol type was added to JavaScript in ES6, and one of the primary reasons for doing so was to safely add extensions to the language without breaking compatibility with code already deployed on the web. We saw an example of this in Chapter 12, where we learned that you can make a class iterable by implementing a method whose “name” is the Symbol Symbol.iterator.
Symbol.iterator is the best-known example of the “well-known Symbols.” These are a set of Symbol values stored as properties of the Symbol() factory function that are used to allow JavaScript code to control certain low-level behaviors of objects and classes. The subsections that follow describe each of these well-known Symbols and explain how they can be used. Symbol.iterator is the best-known example of the “well-known Symbols.” These are a set of Symbol values stored as properties of the Symbol() factory function that are used to allow JavaScript code to control certain low-level behaviors of objects and classes. The subsections that follow describe each of these well-known Symbols and explain how they can be used.
14.4.1 Symbol.iterator and Symbol.asyncIterator ### 14.4.1 Symbol.iterator and Symbol.asyncIterator
The Symbol.iterator and Symbol.asyncIterator Symbols allow objects or classes to make themselves iterable or asynchronously iterable. They were covered in detail in Chapter 12 and §13.4.2, respectively, and are mentioned again here only for completeness. The Symbol.iterator and Symbol.asyncIterator Symbols allow objects or classes to make themselves iterable or asynchronously iterable. They were covered in detail in Chapter 12 and §13.4.2, respectively, and are mentioned again here only for completeness.
14.4.2 Symbol.hasInstance ### 14.4.2 Symbol.hasInstance
When the instanceof operator was described in §4.9.4, we said that the righthand side must be a constructor function and that the expression o instanceof f was evaluated by looking for the value f.prototype within the prototype chain of o. That is still true, but in ES6 and beyond, Symbol.hasInstance provides an alternative. In ES6, if the righthand side of instanceof is any object with a [Symbol.hasInstance] method, then that method is invoked with the lefthand side value as its argument, and the return value of the method, converted to a boolean, becomes the value of the instanceof operator. And, of course, if the value on the righthand side does not have a [Symbol.hasInstance] method but is a function, then the instanceof operator behaves in its ordinary way. When the instanceof operator was described in §4.9.4, we said that the righthand side must be a constructor function and that the expression o instanceof f was evaluated by looking for the value f.prototype within the prototype chain of o. That is still true, but in ES6 and beyond, Symbol.hasInstance provides an alternative. In ES6, if the righthand side of instanceof is any object with a [Symbol.hasInstance] method, then that method is invoked with the lefthand side value as its argument, and the return value of the method, converted to a boolean, becomes the value of the instanceof operator. And, of course, if the value on the righthand side does not have a [Symbol.hasInstance] method but is a function, then the instanceof operator behaves in its ordinary way.
Symbol.hasInstance means that we can use the instanceof operator to do generic type checking with suitably defined pseudotype objects. For example: Symbol.hasInstance means that we can use the instanceof operator to do generic type checking with suitably defined pseudotype objects. For example:
...@@ -261,7 +261,7 @@ let uint8 = { ...@@ -261,7 +261,7 @@ let uint8 = {
Math.PI instanceof uint8 // => false: not an integer Math.PI instanceof uint8 // => false: not an integer
Note that this example is clever but confusing because it uses a nonclass object where a class would normally be expected. It would be just as easy—and clearer to readers of your code—to write a isUint8() function instead of relying on this Symbol.hasInstance behavior. Note that this example is clever but confusing because it uses a nonclass object where a class would normally be expected. It would be just as easy—and clearer to readers of your code—to write a isUint8() function instead of relying on this Symbol.hasInstance behavior.
14.4.3 Symbol.toStringTag ### 14.4.3 Symbol.toStringTag
If you invoke the toString() method of a basic JavaScript object, you get the string “[object Object]”: If you invoke the toString() method of a basic JavaScript object, you get the string “[object Object]”:
{}.toString() // => "[object Object]" {}.toString() // => "[object Object]"
...@@ -302,7 +302,7 @@ class Range { ...@@ -302,7 +302,7 @@ class Range {
let r = new Range(1, 10); let r = new Range(1, 10);
Object.prototype.toString.call(r) // => "[object Range]" Object.prototype.toString.call(r) // => "[object Range]"
classof(r) // => "Range" classof(r) // => "Range"
14.4.4 Symbol.species ### 14.4.4 Symbol.species
Prior to ES6, JavaScript did not provide any real way to create robust subclasses of built-in classes like Array. In ES6, however, you can extend any built-in class simply by using the class and extends keywords. §9.5.2 demonstrated that with this simple subclass of Array: Prior to ES6, JavaScript did not provide any real way to create robust subclasses of built-in classes like Array. In ES6, however, you can extend any built-in class simply by using the class and extends keywords. §9.5.2 demonstrated that with this simple subclass of Array:
// A trivial Array subclass that adds getters for the first and last elements. // A trivial Array subclass that adds getters for the first and last elements.
...@@ -350,7 +350,7 @@ e.last // => 3 ...@@ -350,7 +350,7 @@ e.last // => 3
f.last // => undefined: f is a regular array with no last getter f.last // => undefined: f is a regular array with no last getter
Creating useful subclasses of Array was the primary use case that motivated the introduction of Symbol.species, but it is not the only place that this well-known Symbol is used. Typed array classes use the Symbol in the same way that the Array class does. Similarly, the slice() method of ArrayBuffer looks at the Symbol.species property of this.constructor instead of simply creating a new ArrayBuffer. And Promise methods like then() that return new Promise objects create those objects via this species protocol as well. Finally, if you find yourself subclassing Map (for example) and defining methods that return new Map objects, you might want to use Symbol.species yourself for the benefit of subclasses of your subclass. Creating useful subclasses of Array was the primary use case that motivated the introduction of Symbol.species, but it is not the only place that this well-known Symbol is used. Typed array classes use the Symbol in the same way that the Array class does. Similarly, the slice() method of ArrayBuffer looks at the Symbol.species property of this.constructor instead of simply creating a new ArrayBuffer. And Promise methods like then() that return new Promise objects create those objects via this species protocol as well. Finally, if you find yourself subclassing Map (for example) and defining methods that return new Map objects, you might want to use Symbol.species yourself for the benefit of subclasses of your subclass.
14.4.5 Symbol.isConcatSpreadable ### 14.4.5 Symbol.isConcatSpreadable
The Array method concat() is one of the methods described in the previous section that uses Symbol.species to determine what constructor to use for the returned array. But concat() also uses Symbol.isConcatSpreadable. Recall from §7.8.3 that the concat() method of an array treats its this value and its array arguments differently than its nonarray arguments: nonarray arguments are simply appended to the new array, but the this array and any array arguments are flattened or “spread” so that the elements of the array are concatenated rather than the array argument itself. The Array method concat() is one of the methods described in the previous section that uses Symbol.species to determine what constructor to use for the returned array. But concat() also uses Symbol.isConcatSpreadable. Recall from §7.8.3 that the concat() method of an array treats its this value and its array arguments differently than its nonarray arguments: nonarray arguments are simply appended to the new array, but the this array and any array arguments are flattened or “spread” so that the elements of the array are concatenated rather than the array argument itself.
Before ES6, concat() just used Array.isArray() to determine whether to treat a value as an array or not. In ES6, the algorithm is changed slightly: if the argument (or the this value) to concat() is an object and has a property with the symbolic name Symbol.isConcatSpreadable, then the boolean value of that property is used to determine whether the argument should be “spread.” If no such property exists, then Array.isArray() is used as in previous versions of the language. Before ES6, concat() just used Array.isArray() to determine whether to treat a value as an array or not. In ES6, the algorithm is changed slightly: if the argument (or the this value) to concat() is an object and has a property with the symbolic name Symbol.isConcatSpreadable, then the boolean value of that property is used to determine whether the argument should be “spread.” If no such property exists, then Array.isArray() is used as in previous versions of the language.
...@@ -372,7 +372,7 @@ class NonSpreadableArray extends Array { ...@@ -372,7 +372,7 @@ class NonSpreadableArray extends Array {
} }
let a = new NonSpreadableArray(1,2,3); let a = new NonSpreadableArray(1,2,3);
[].concat(a).length // => 1; (would be 3 elements long if a was spread) [].concat(a).length // => 1; (would be 3 elements long if a was spread)
14.4.6 Pattern-Matching Symbols ### 14.4.6 Pattern-Matching Symbols
§11.3.2 documented the String methods that perform pattern-matching operations using a RegExp argument. In ES6 and later, these methods have been generalized to work with RegExp objects or any object that defines pattern-matching behavior via properties with symbolic names. For each of the string methods match(), matchAll(), search(), replace(), and split(), there is a corresponding well-known Symbol: Symbol.match, Symbol.search, and so on. §11.3.2 documented the String methods that perform pattern-matching operations using a RegExp argument. In ES6 and later, these methods have been generalized to work with RegExp objects or any object that defines pattern-matching behavior via properties with symbolic names. For each of the string methods match(), matchAll(), search(), replace(), and split(), there is a corresponding well-known Symbol: Symbol.match, Symbol.search, and so on.
RegExps are a general and very powerful way to describe textual patterns, but they can be complicated and not well suited to fuzzy matching. With the generalized string methods, you can define your own pattern classes using the well-known Symbol methods to provide custom matching. For example, you could perform string comparisons using Intl.Collator (see §11.7.3) to ignore accents when matching. Or you could define a pattern class based on the Soundex algorithm to match words based on their approximate sounds or to loosely match strings up to a given Levenshtein distance. RegExps are a general and very powerful way to describe textual patterns, but they can be complicated and not well suited to fuzzy matching. With the generalized string methods, you can define your own pattern classes using the well-known Symbol methods to provide custom matching. For example, you could perform string comparisons using Intl.Collator (see §11.7.3) to ignore accents when matching. Or you could define a pattern class based on the Soundex algorithm to match words based on their approximate sounds or to loosely match strings up to a given Levenshtein distance.
...@@ -418,7 +418,7 @@ match[0] // => "docs/js.txt" ...@@ -418,7 +418,7 @@ match[0] // => "docs/js.txt"
match[1] // => "js" match[1] // => "js"
match.index // => 0 match.index // => 0
"docs/js.txt".replace(pattern, "web/$1.htm") // => "web/js.htm" "docs/js.txt".replace(pattern, "web/$1.htm") // => "web/js.htm"
14.4.7 Symbol.toPrimitive ### 14.4.7 Symbol.toPrimitive
§3.9.3 explained that JavaScript has three slightly different algorithms for converting objects to primitive values. Loosely speaking, for conversions where a string value is expected or preferred, JavaScript invokes an object’s toString() method first and falls back on the valueOf() method if toString() is not defined or does not return a primitive value. For conversions where a numeric value is preferred, JavaScript tries the valueOf() method first and falls back on toString() if valueOf() is not defined or if it does not return a primitive value. And finally, in cases where there is no preference, it lets the class decide how to do the conversion. Date objects convert using toString() first, and all other types try valueOf() first. §3.9.3 explained that JavaScript has three slightly different algorithms for converting objects to primitive values. Loosely speaking, for conversions where a string value is expected or preferred, JavaScript invokes an object’s toString() method first and falls back on the valueOf() method if toString() is not defined or does not return a primitive value. For conversions where a numeric value is preferred, JavaScript tries the valueOf() method first and falls back on toString() if valueOf() is not defined or if it does not return a primitive value. And finally, in cases where there is no preference, it lets the class decide how to do the conversion. Date objects convert using toString() first, and all other types try valueOf() first.
In ES6, the well-known Symbol Symbol.toPrimitive allows you to override this default object-to-primitive behavior and gives you complete control over how instances of your own classes will be converted to primitive values. To do this, define a method with this symbolic name. The method must return a primitive value that somehow represents the object. The method you define will be invoked with a single string argument that tells you what kind of conversion JavaScript is trying to do on your object: In ES6, the well-known Symbol Symbol.toPrimitive allows you to override this default object-to-primitive behavior and gives you complete control over how instances of your own classes will be converted to primitive values. To do this, define a method with this symbolic name. The method must return a primitive value that somehow represents the object. The method you define will be invoked with a single string argument that tells you what kind of conversion JavaScript is trying to do on your object:
...@@ -431,11 +431,11 @@ If the argument is "default", it means that JavaScript is converting your object ...@@ -431,11 +431,11 @@ If the argument is "default", it means that JavaScript is converting your object
Many classes can ignore the argument and simply return the same primitive value in all cases. If you want instances of your class to be comparable and sortable with < and >, then that is a good reason to define a [Symbol.toPrimitive] method. Many classes can ignore the argument and simply return the same primitive value in all cases. If you want instances of your class to be comparable and sortable with < and >, then that is a good reason to define a [Symbol.toPrimitive] method.
14.4.8 Symbol.unscopables ### 14.4.8 Symbol.unscopables
The final well-known Symbol that we’ll cover here is an obscure one that was introduced as a workaround for compatibility issues caused by the deprecated with statement. Recall that the with statement takes an object and executes its statement body as if it were in a scope where the properties of that object were variables. This caused compatibility problems when new methods were added to the Array class, and it broke some existing code. Symbol.unscopables is the result. In ES6 and later, the with statement has been slightly modified. When used with an object o, a with statement computes Object.keys(o[Symbol.unscopables]||{}) and ignores properties whose names are in the resulting array when creating the simulated scope in which to execute its body. ES6 uses this to add new methods to Array.prototype without breaking existing code on the web. This means that you can find a list of the newest Array methods by evaluating: The final well-known Symbol that we’ll cover here is an obscure one that was introduced as a workaround for compatibility issues caused by the deprecated with statement. Recall that the with statement takes an object and executes its statement body as if it were in a scope where the properties of that object were variables. This caused compatibility problems when new methods were added to the Array class, and it broke some existing code. Symbol.unscopables is the result. In ES6 and later, the with statement has been slightly modified. When used with an object o, a with statement computes Object.keys(o[Symbol.unscopables]||{}) and ignores properties whose names are in the resulting array when creating the simulated scope in which to execute its body. ES6 uses this to add new methods to Array.prototype without breaking existing code on the web. This means that you can find a list of the newest Array methods by evaluating:
let newArrayMethods = Object.keys(Array.prototype[Symbol.unscopables]); let newArrayMethods = Object.keys(Array.prototype[Symbol.unscopables]);
14.5 Template Tags ## 14.5 Template Tags
Strings within backticks are known as “template literals” and were covered in §3.3.4. When an expression whose value is a function is followed by a template literal, it turns into a function invocation, and we call it a “tagged template literal.” Defining a new tag function for use with tagged template literals can be thought of as metaprogramming, because tagged templates are often used to define DSLs—domain-specific languages—and defining a new tag function is like adding new syntax to JavaScript. Tagged template literals have been adopted by a number of frontend JavaScript packages. The GraphQL query language uses a gql`` tag function to allow queries to be embedded within JavaScript code. And the Emotion library uses a css`` tag function to enable CSS styles to be embedded in JavaScript. This section demonstrates how to write your own tag functions like these. Strings within backticks are known as “template literals” and were covered in §3.3.4. When an expression whose value is a function is followed by a template literal, it turns into a function invocation, and we call it a “tagged template literal.” Defining a new tag function for use with tagged template literals can be thought of as metaprogramming, because tagged templates are often used to define DSLs—domain-specific languages—and defining a new tag function is like adding new syntax to JavaScript. Tagged template literals have been adopted by a number of frontend JavaScript packages. The GraphQL query language uses a gql`` tag function to allow queries to be embedded within JavaScript code. And the Emotion library uses a css`` tag function to enable CSS styles to be embedded in JavaScript. This section demonstrates how to write your own tag functions like these.
There is nothing special about tag functions: they are ordinary JavaScript functions, and no special syntax is required to define them. When a function expression is followed by a template literal, the function is invoked. The first argument is an array of strings, and this is followed by zero or more additional arguments, which can have values of any type. There is nothing special about tag functions: they are ordinary JavaScript functions, and no special syntax is required to define them. When a function expression is followed by a template literal, the function is invoked. The first argument is an array of strings, and this is followed by zero or more additional arguments, which can have values of any type.
...@@ -485,7 +485,7 @@ let filePattern = glob`${root}/*.html`; // A RegExp alternative ...@@ -485,7 +485,7 @@ let filePattern = glob`${root}/*.html`; // A RegExp alternative
"/tmp/test.html".match(filePattern)[1] // => "test" "/tmp/test.html".match(filePattern)[1] // => "test"
One of the features mentioned in passing in §3.3.4 is the String.raw`` tag function that returns a string in its “raw” form without interpreting any of the backslash escape sequences. This is implemented using a feature of tag function invocation that we have not discussed yet. When a tag function is invoked, we’ve seen that its first argument is an array of strings. But this array also has a property named raw, and the value of that property is another array of strings, with the same number of elements. The argument array includes strings that have had escape sequences interpreted as usual. And the raw array includes strings in which escape sequences are not interpreted. This obscure feature is important if you want to define a DSL with a grammar that uses backslashes. For example, if we wanted our glob`` tag function to support pattern matching on Windows-style paths (which use backslashes instead of forward slashes) and we did not want users of the tag to have to double every backslash, we could rewrite that function to use strings.raw[] instead of strings[]. The downside, of course, would be that we could no longer use escapes like \u in our glob literals. One of the features mentioned in passing in §3.3.4 is the String.raw`` tag function that returns a string in its “raw” form without interpreting any of the backslash escape sequences. This is implemented using a feature of tag function invocation that we have not discussed yet. When a tag function is invoked, we’ve seen that its first argument is an array of strings. But this array also has a property named raw, and the value of that property is another array of strings, with the same number of elements. The argument array includes strings that have had escape sequences interpreted as usual. And the raw array includes strings in which escape sequences are not interpreted. This obscure feature is important if you want to define a DSL with a grammar that uses backslashes. For example, if we wanted our glob`` tag function to support pattern matching on Windows-style paths (which use backslashes instead of forward slashes) and we did not want users of the tag to have to double every backslash, we could rewrite that function to use strings.raw[] instead of strings[]. The downside, of course, would be that we could no longer use escapes like \u in our glob literals.
14.6 The Reflect API ## 14.6 The Reflect API
The Reflect object is not a class; like the Math object, its properties simply define a collection of related functions. These functions, added in ES6, define an API for “reflecting upon” objects and their properties. There is little new functionality here: the Reflect object defines a convenient set of functions, all in a single namespace, that mimic the behavior of core language syntax and duplicate the features of various pre-existing Object functions. The Reflect object is not a class; like the Math object, its properties simply define a collection of related functions. These functions, added in ES6, define an API for “reflecting upon” objects and their properties. There is little new functionality here: the Reflect object defines a convenient set of functions, all in a single namespace, that mimic the behavior of core language syntax and duplicate the features of various pre-existing Object functions.
Although the Reflect functions do not provide any new features, they do group the features together in one convenient API. And, importantly, the set of Reflect functions maps one-to-one with the set of Proxy handler methods that we’ll learn about in §14.7. Although the Reflect functions do not provide any new features, they do group the features together in one convenient API. And, importantly, the set of Reflect functions maps one-to-one with the set of Proxy handler methods that we’ll learn about in §14.7.
...@@ -531,7 +531,7 @@ This function sets the property with the specified name of the object o to the s ...@@ -531,7 +531,7 @@ This function sets the property with the specified name of the object o to the s
Reflect.setPrototypeOf(o, p) Reflect.setPrototypeOf(o, p)
This function sets the prototype of the object o to p, returning true on success and false on failure (which can occur if o is not extensible or if the operation would cause a circular prototype chain). It throws a TypeError if o is not an object or if p is neither an object nor null. Object.setPrototypeOf() is similar, but returns o on success and throws TypeError on failure. Remember that calling either of these functions is likely to make your code slower by disrupting JavaScript interpreter optimizations. This function sets the prototype of the object o to p, returning true on success and false on failure (which can occur if o is not extensible or if the operation would cause a circular prototype chain). It throws a TypeError if o is not an object or if p is neither an object nor null. Object.setPrototypeOf() is similar, but returns o on success and throws TypeError on failure. Remember that calling either of these functions is likely to make your code slower by disrupting JavaScript interpreter optimizations.
14.7 Proxy Objects ## 14.7 Proxy Objects
The Proxy class, available in ES6 and later, is JavaScript’s most powerful metaprogramming feature. It allows us to write code that alters the fundamental behavior of JavaScript objects. The Reflect API described in §14.6 is a set of functions that gives us direct access to a set of fundamental operations on JavaScript objects. What the Proxy class does is allows us a way to implement those fundamental operations ourselves and create objects that behave in ways that are not possible for ordinary objects. The Proxy class, available in ES6 and later, is JavaScript’s most powerful metaprogramming feature. It allows us to write code that alters the fundamental behavior of JavaScript objects. The Reflect API described in §14.6 is a set of functions that gives us direct access to a set of fundamental operations on JavaScript objects. What the Proxy class does is allows us a way to implement those fundamental operations ourselves and create objects that behave in ways that are not possible for ordinary objects.
When we create a Proxy object, we specify two other objects, the target object and the handlers object: When we create a Proxy object, we specify two other objects, the target object and the handlers object:
...@@ -748,7 +748,7 @@ The second chunk of logging output might remind us that the function we pass to ...@@ -748,7 +748,7 @@ The second chunk of logging output might remind us that the function we pass to
The third chunk of logging output shows us that the for/of loop works by looking for a method with symbolic name [Symbol.iterator]. It also demonstrates that the Array class’s implementation of this iterator method is careful to check the array length at every iteration and does not assume that the array length remains constant during the iteration. The third chunk of logging output shows us that the for/of loop works by looking for a method with symbolic name [Symbol.iterator]. It also demonstrates that the Array class’s implementation of this iterator method is careful to check the array length at every iteration and does not assume that the array length remains constant during the iteration.
14.7.1 Proxy Invariants ### 14.7.1 Proxy Invariants
The readOnlyProxy() function defined earlier creates Proxy objects that are effectively frozen: any attempt to alter a property value or property attribute or to add or remove properties will throw an exception. But as long as the target object is not frozen, we’ll find that if we can query the proxy with Reflect.isExtensible() and Reflect.getOwnPropertyDescriptor(), and it will tell us that we should be able to set, add, and delete properties. So readOnlyProxy() creates objects in an inconsistent state. We could fix this by adding isExtensible() and getOwnPropertyDescriptor() handlers, or we can just live with this kind of minor inconsistency. The readOnlyProxy() function defined earlier creates Proxy objects that are effectively frozen: any attempt to alter a property value or property attribute or to add or remove properties will throw an exception. But as long as the target object is not frozen, we’ll find that if we can query the proxy with Reflect.isExtensible() and Reflect.getOwnPropertyDescriptor(), and it will tell us that we should be able to set, add, and delete properties. So readOnlyProxy() creates objects in an inconsistent state. We could fix this by adding isExtensible() and getOwnPropertyDescriptor() handlers, or we can just live with this kind of minor inconsistency.
The Proxy handler API allows us to define objects with major inconsistencies, however, and in this case, the Proxy class itself will prevent us from creating Proxy objects that are inconsistent in a bad way. At the start of this section, we described proxies as objects with no behavior of their own because they simply forward all operations to the handlers object and the target object. But this is not entirely true: after forwarding an operation, the Proxy class performs some sanity checks on the result to ensure important JavaScript invariants are not being violated. If it detects a violation, the proxy will throw a TypeError instead of letting the operation proceed. The Proxy handler API allows us to define objects with major inconsistencies, however, and in this case, the Proxy class itself will prevent us from creating Proxy objects that are inconsistent in a bad way. At the start of this section, we described proxies as objects with no behavior of their own because they simply forward all operations to the handlers object and the target object. But this is not entirely true: after forwarding an operation, the Proxy class performs some sanity checks on the result to ensure important JavaScript invariants are not being violated. If it detects a violation, the proxy will throw a TypeError instead of letting the operation proceed.
...@@ -765,7 +765,7 @@ let proxy = new Proxy(target, { get() { return 99; }}); ...@@ -765,7 +765,7 @@ let proxy = new Proxy(target, { get() { return 99; }});
proxy.x; // !TypeError: value returned by get() doesn't match target proxy.x; // !TypeError: value returned by get() doesn't match target
Proxy enforces a number of additional invariants, almost all of them having to do with non-extensible target objects and nonconfigurable properties on the target object. Proxy enforces a number of additional invariants, almost all of them having to do with non-extensible target objects and nonconfigurable properties on the target object.
14.8 Summary ## 14.8 Summary
In this chapter, you have learned: In this chapter, you have learned:
JavaScript objects have an extensible attribute and object properties have writable, enumerable, and configurable attributes, as well as a value and a getter and/or setter attribute. You can use these attributes to “lock down” your objects in various ways, including creating “sealed” and “frozen” objects. JavaScript objects have an extensible attribute and object properties have writable, enumerable, and configurable attributes, as well as a value and a getter and/or setter attribute. You can use these attributes to “lock down” your objects in various ways, including creating “sealed” and “frozen” objects.
......
...@@ -43,10 +43,10 @@ Poorly designed APIs that have been replaced by better ones. In the early days o ...@@ -43,10 +43,10 @@ Poorly designed APIs that have been replaced by better ones. In the early days o
Browser vendors may need to support these legacy APIs for the foreseeable future in order to ensure backward compatibility, but there is no longer any need for this book to document them or for you to learn about them. The web platform has matured and stabilized, and if you are a seasoned web developer who remembers the fourth or fifth edition of this book, then you may have as much outdated knowledge to forget as you have new material to learn. Browser vendors may need to support these legacy APIs for the foreseeable future in order to ensure backward compatibility, but there is no longer any need for this book to document them or for you to learn about them. The web platform has matured and stabilized, and if you are a seasoned web developer who remembers the fourth or fifth edition of this book, then you may have as much outdated knowledge to forget as you have new material to learn.
15.1 Web Programming Basics ## 15.1 Web Programming Basics
This section explains how JavaScript programs for the web are structured, how they are loaded into a web browser, how they obtain input, how they produce output, and how they run asynchronously by responding to events. This section explains how JavaScript programs for the web are structured, how they are loaded into a web browser, how they obtain input, how they produce output, and how they run asynchronously by responding to events.
15.1.1 JavaScript in HTML <script> Tags ### 15.1.1 JavaScript in HTML <script> Tags
Web browsers display HTML documents. If you want a web browser to execute JavaScript code, you must include (or reference) that code from an HTML document, and this is what the HTML <script> tag does. Web browsers display HTML documents. If you want a web browser to execute JavaScript code, you must include (or reference) that code from an HTML document, and this is what the HTML <script> tag does.
JavaScript code can appear inline within an HTML file between <script> and </script> tags. Here, for example, is an HTML file that includes a script tag with JavaScript code that dynamically updates one element of the document to make it behave like a digital clock: JavaScript code can appear inline within an HTML file between <script> and </script> tags. Here, for example, is an HTML file that includes a script tag with JavaScript code that dynamically updates one element of the document to make it behave like a digital clock:
...@@ -142,7 +142,7 @@ function importScript(url) { ...@@ -142,7 +142,7 @@ function importScript(url) {
} }
This importScript() function uses DOM APIs (§15.3) to create a new <script> tag and add it to the document <head>. And it uses event handlers (§15.2) to determine when the script has loaded successfully or when loading has failed. This importScript() function uses DOM APIs (§15.3) to create a new <script> tag and add it to the document <head>. And it uses event handlers (§15.2) to determine when the script has loaded successfully or when loading has failed.
15.1.2 The Document Object Model ### 15.1.2 The Document Object Model
One of the most important objects in client-side JavaScript programming is the Document object—which represents the HTML document that is displayed in a browser window or tab. The API for working with HTML documents is known as the Document Object Model, or DOM, and it is covered in detail in §15.3. But the DOM is so central to client-side JavaScript programming that it deserves to be introduced here. One of the most important objects in client-side JavaScript programming is the Document object—which represents the HTML document that is displayed in a browser window or tab. The API for working with HTML documents is known as the Document Object Model, or DOM, and it is covered in detail in §15.3. But the DOM is so central to client-side JavaScript programming that it deserves to be introduced here.
HTML documents contain HTML elements nested within one another, forming a tree. Consider the following simple HTML document: HTML documents contain HTML elements nested within one another, forming a tree. Consider the following simple HTML document:
...@@ -168,14 +168,14 @@ The DOM API includes methods for creating new Element and Text nodes, and for in ...@@ -168,14 +168,14 @@ The DOM API includes methods for creating new Element and Text nodes, and for in
There is a JavaScript class corresponding to each HTML tag type, and each occurrence of the tag in a document is represented by an instance of the class. The <body> tag, for example, is represented by an instance of HTMLBodyElement, and a <table> tag is represented by an instance of HTMLTableElement. The JavaScript element objects have properties that correspond to the HTML attributes of the tags. For example, instances of HTMLImageElement, which represent <img> tags, have a src property that corresponds to the src attribute of the tag. The initial value of the src property is the attribute value that appears in the HTML tag, and setting this property with JavaScript changes the value of the HTML attribute (and causes the browser to load and display a new image). Most of the JavaScript element classes just mirror the attributes of an HTML tag, but some define additional methods. The HTMLAudioElement and HTMLVideoElement classes, for example, define methods like play() and pause() for controlling playback of audio and video files. There is a JavaScript class corresponding to each HTML tag type, and each occurrence of the tag in a document is represented by an instance of the class. The <body> tag, for example, is represented by an instance of HTMLBodyElement, and a <table> tag is represented by an instance of HTMLTableElement. The JavaScript element objects have properties that correspond to the HTML attributes of the tags. For example, instances of HTMLImageElement, which represent <img> tags, have a src property that corresponds to the src attribute of the tag. The initial value of the src property is the attribute value that appears in the HTML tag, and setting this property with JavaScript changes the value of the HTML attribute (and causes the browser to load and display a new image). Most of the JavaScript element classes just mirror the attributes of an HTML tag, but some define additional methods. The HTMLAudioElement and HTMLVideoElement classes, for example, define methods like play() and pause() for controlling playback of audio and video files.
15.1.3 The Global Object in Web Browsers ### 15.1.3 The Global Object in Web Browsers
There is one global object per browser window or tab (§3.7). All of the JavaScript code (except code running in worker threads; see §15.13) running in that window shares this single global object. This is true regardless of how many scripts or modules are in the document: all the scripts and modules of a document share a single global object; if one script defines a property on that object, that property is visible to all the other scripts as well. There is one global object per browser window or tab (§3.7). All of the JavaScript code (except code running in worker threads; see §15.13) running in that window shares this single global object. This is true regardless of how many scripts or modules are in the document: all the scripts and modules of a document share a single global object; if one script defines a property on that object, that property is visible to all the other scripts as well.
The global object is where JavaScript’s standard library is defined—the parseInt() function, the Math object, the Set class, and so on. In web browsers, the global object also contains the main entry points of various web APIs. For example, the document property represents the currently displayed document, the fetch() method makes HTTP network requests, and the Audio() constructor allows JavaScript programs to play sounds. The global object is where JavaScript’s standard library is defined—the parseInt() function, the Math object, the Set class, and so on. In web browsers, the global object also contains the main entry points of various web APIs. For example, the document property represents the currently displayed document, the fetch() method makes HTTP network requests, and the Audio() constructor allows JavaScript programs to play sounds.
In web browsers, the global object does double duty: in addition to defining built-in types and functions, it also represents the current web browser window and defines properties like history (§15.10.2), which represent the window’s browsing history, and innerWidth, which holds the window’s width in pixels. One of the properties of this global object is named window, and its value is the global object itself. This means that you can simply type window to refer to the global object in your client-side code. When using window-specific features, it is often a good idea to include a window. prefix: window.innerWidth is clearer than innerWidth, for example. In web browsers, the global object does double duty: in addition to defining built-in types and functions, it also represents the current web browser window and defines properties like history (§15.10.2), which represent the window’s browsing history, and innerWidth, which holds the window’s width in pixels. One of the properties of this global object is named window, and its value is the global object itself. This means that you can simply type window to refer to the global object in your client-side code. When using window-specific features, it is often a good idea to include a window. prefix: window.innerWidth is clearer than innerWidth, for example.
15.1.4 Scripts Share a Namespace ### 15.1.4 Scripts Share a Namespace
With modules, the constants, variables, functions, and classes defined at the top level (i.e., outside of any function or class definition) of the module are private to the module unless they are explicitly exported, in which case, they can be selectively imported by other modules. (Note that this property of modules is honored by code-bundling tools as well.) With modules, the constants, variables, functions, and classes defined at the top level (i.e., outside of any function or class definition) of the module are private to the module unless they are explicitly exported, in which case, they can be selectively imported by other modules. (Note that this property of modules is honored by code-bundling tools as well.)
With non-module scripts, however, the situation is completely different. If the top-level code in a script defines a constant, variable, function, or class, that declaration will be visible to all other scripts in the same document. If one script defines a function f() and another script defines a class c, then a third script can invoke the function and instantiate the class without having to take any action to import them. So if you are not using modules, the independent scripts in your document share a single namespace and behave as if they are all part of a single larger script. This can be convenient for small programs, but the need to avoid naming conflicts can become problematic for larger programs, especially when some of the scripts are third-party libraries. With non-module scripts, however, the situation is completely different. If the top-level code in a script defines a constant, variable, function, or class, that declaration will be visible to all other scripts in the same document. If one script defines a function f() and another script defines a class c, then a third script can invoke the function and instantiate the class without having to take any action to import them. So if you are not using modules, the independent scripts in your document share a single namespace and behave as if they are all part of a single larger script. This can be convenient for small programs, but the need to avoid naming conflicts can become problematic for larger programs, especially when some of the scripts are third-party libraries.
...@@ -184,7 +184,7 @@ There are some historical quirks with how this shared namespace works. var and f ...@@ -184,7 +184,7 @@ There are some historical quirks with how this shared namespace works. var and f
To summarize: in modules, top-level declarations are scoped to the module and can be explicitly exported. In nonmodule scripts, however, top-level declarations are scoped to the containing document, and the declarations are shared by all scripts in the document. Older var and function declarations are shared via properties of the global object. Newer const, let, and class declarations are also shared and have the same document scope, but they do not exist as properties of any object that JavaScript code has access to. To summarize: in modules, top-level declarations are scoped to the module and can be explicitly exported. In nonmodule scripts, however, top-level declarations are scoped to the containing document, and the declarations are shared by all scripts in the document. Older var and function declarations are shared via properties of the global object. Newer const, let, and class declarations are also shared and have the same document scope, but they do not exist as properties of any object that JavaScript code has access to.
15.1.5 Execution of JavaScript Programs ### 15.1.5 Execution of JavaScript Programs
There is no formal definition of a program in client-side JavaScript, but we can say that a JavaScript program consists of all the JavaScript code in, or referenced from, a document. These separate bits of code share a single global Window object, which gives them access to the same underlying Document object representing the HTML document. Scripts that are not modules additionally share a top-level namespace. There is no formal definition of a program in client-side JavaScript, but we can say that a JavaScript program consists of all the JavaScript code in, or referenced from, a document. These separate bits of code share a single global Window object, which gives them access to the same underlying Document object representing the HTML document. Scripts that are not modules additionally share a top-level namespace.
If a web page includes an embedded frame (using the <iframe> element), the JavaScript code in the embedded document has a different global object and Document object than the code in the embedding document, and it can be considered a separate JavaScript program. Remember, though, that there is no formal definition of what the boundaries of a JavaScript program are. If the container document and the contained document are both loaded from the same server, the code in one document can interact with the code in the other, and you can treat them as two interacting parts of a single program, if you wish. §15.13.6 explains how a JavaScript program can send and receive messages to and from JavaScript code running in an <iframe>. If a web page includes an embedded frame (using the <iframe> element), the JavaScript code in the embedded document has a different global object and Document object than the code in the embedding document, and it can be considered a separate JavaScript program. Remember, though, that there is no formal definition of what the boundaries of a JavaScript program are. If the container document and the contained document are both loaded from the same server, the code in one document can interact with the code in the other, and you can treat them as two interacting parts of a single program, if you wish. §15.13.6 explains how a JavaScript program can send and receive messages to and from JavaScript code running in an <iframe>.
...@@ -223,7 +223,7 @@ The document is completely parsed at this point, but the browser may still be wa ...@@ -223,7 +223,7 @@ The document is completely parsed at this point, but the browser may still be wa
From this point on, event handlers are invoked asynchronously in response to user input events, network events, timer expirations, and so on. From this point on, event handlers are invoked asynchronously in response to user input events, network events, timer expirations, and so on.
15.1.6 Program Input and Output ### 15.1.6 Program Input and Output
Like any program, client-side JavaScript programs process input data to produce output data. There are a variety of inputs available: Like any program, client-side JavaScript programs process input data to produce output data. There are a variety of inputs available:
The content of the document itself, which JavaScript code can access with the DOM API (§15.3). The content of the document itself, which JavaScript code can access with the DOM API (§15.3).
...@@ -238,7 +238,7 @@ The global navigator property provides access to information about the web brows ...@@ -238,7 +238,7 @@ The global navigator property provides access to information about the web brows
Client-side JavaScript typically produces output, when it needs to, by manipulating the HTML document with the DOM API (§15.3) or by using a higher-level framework such as React or Angular to manipulate the document. Client-side code can also use console.log() and related methods (§11.8) to produce output. But this output is only visible in the web developer console, so it is useful when debugging, but not for user-visible output. Client-side JavaScript typically produces output, when it needs to, by manipulating the HTML document with the DOM API (§15.3) or by using a higher-level framework such as React or Angular to manipulate the document. Client-side code can also use console.log() and related methods (§11.8) to produce output. But this output is only visible in the web developer console, so it is useful when debugging, but not for user-visible output.
15.1.7 Program Errors ### 15.1.7 Program Errors
Unlike applications (such as Node applications) that run directly on top of the OS, JavaScript programs in a web browser can’t really “crash.” If an exception occurs while your JavaScript program is running, and if you do not have a catch statement to handle it, an error message will be displayed in the developer console, but any event handlers that have been registered keep running and responding to events. Unlike applications (such as Node applications) that run directly on top of the OS, JavaScript programs in a web browser can’t really “crash.” If an exception occurs while your JavaScript program is running, and if you do not have a catch statement to handle it, an error message will be displayed in the developer console, but any event handlers that have been registered keep running and responding to events.
If you would like to define an error handler of last resort to be invoked when this kind of uncaught exception occurs, set the onerror property of the Window object to an error handler function. When an uncaught exception propagates all the way up the call stack and an error message is about to be displayed in the developer console, the window.onerror function will be invoked with three string arguments. The first argument to window.onerror is a message describing the error. The second argument is a string that contains the URL of the JavaScript code that caused the error. The third argument is the line number within the document where the error occurred. If the onerror handler returns true, it tells the browser that the handler has handled the error and that no further action is necessary—in other words, the browser should not display its own error message. If you would like to define an error handler of last resort to be invoked when this kind of uncaught exception occurs, set the onerror property of the Window object to an error handler function. When an uncaught exception propagates all the way up the call stack and an error message is about to be displayed in the developer console, the window.onerror function will be invoked with three string arguments. The first argument to window.onerror is a message describing the error. The second argument is a string that contains the URL of the JavaScript code that caused the error. The third argument is the line number within the document where the error occurred. If the onerror handler returns true, it tells the browser that the handler has handled the error and that no further action is necessary—in other words, the browser should not display its own error message.
...@@ -247,7 +247,7 @@ When a Promise is rejected and there is no .catch() function to handle it, that ...@@ -247,7 +247,7 @@ When a Promise is rejected and there is no .catch() function to handle it, that
It is not often necessary to define onerror or onunhandledrejection handlers, but it can be quite useful as a telemetry mechanism if you want to report client-side errors to the server (using the fetch() function to make an HTTP POST request, for example) so that you can get information about unexpected errors that happen in your users’ browsers. It is not often necessary to define onerror or onunhandledrejection handlers, but it can be quite useful as a telemetry mechanism if you want to report client-side errors to the server (using the fetch() function to make an HTTP POST request, for example) so that you can get information about unexpected errors that happen in your users’ browsers.
15.1.8 The Web Security Model ### 15.1.8 The Web Security Model
The fact that web pages can execute arbitrary JavaScript code on your personal device has clear security implications, and browser vendors have worked hard to balance two competing goals: The fact that web pages can execute arbitrary JavaScript code on your personal device has clear security implications, and browser vendors have worked hard to balance two competing goals:
Defining powerful client-side APIs to enable useful web applications Defining powerful client-side APIs to enable useful web applications
...@@ -309,7 +309,7 @@ Another approach to the problem of XSS is to structure your web applications so ...@@ -309,7 +309,7 @@ Another approach to the problem of XSS is to structure your web applications so
Cross-site scripting is a pernicious vulnerability whose roots go deep into the architecture of the web. It is worth understanding this vulnerability in-depth, but further discussion is beyond the scope of this book. There are many online resources to help you defend against cross-site scripting. Cross-site scripting is a pernicious vulnerability whose roots go deep into the architecture of the web. It is worth understanding this vulnerability in-depth, but further discussion is beyond the scope of this book. There are many online resources to help you defend against cross-site scripting.
15.2 Events ## 15.2 Events
Client-side JavaScript programs use an asynchronous event-driven programming model. In this style of programming, the web browser generates an event whenever something interesting happens to the document or browser or to some element or object associated with it. For example, the web browser generates an event when it finishes loading a document, when the user moves the mouse over a hyperlink, or when the user strikes a key on the keyboard. If a JavaScript application cares about a particular type of event, it can register one or more functions to be invoked when events of that type occur. Note that this is not unique to web programming: all applications with graphical user interfaces are designed this way—they sit around waiting to be interacted with (i.e., they wait for events to occur), and then they respond. Client-side JavaScript programs use an asynchronous event-driven programming model. In this style of programming, the web browser generates an event whenever something interesting happens to the document or browser or to some element or object associated with it. For example, the web browser generates an event when it finishes loading a document, when the user moves the mouse over a hyperlink, or when the user strikes a key on the keyboard. If a JavaScript application cares about a particular type of event, it can register one or more functions to be invoked when events of that type occur. Note that this is not unique to web programming: all applications with graphical user interfaces are designed this way—they sit around waiting to be interacted with (i.e., they wait for events to occur), and then they respond.
In client-side JavaScript, events can occur on any element within an HTML document, and this fact makes the event model of web browsers significantly more complex than Node’s event model. We begin this section with some important definitions that help to explain that event model: In client-side JavaScript, events can occur on any element within an HTML document, and this fact makes the event model of web browsers significantly more complex than Node’s event model. We begin this section with some important definitions that help to explain that event model:
...@@ -331,7 +331,7 @@ This is the process by which the browser decides which objects to trigger event ...@@ -331,7 +331,7 @@ This is the process by which the browser decides which objects to trigger event
Some events have default actions associated with them. When a click event occurs on a hyperlink, for example, the default action is for the browser to follow the link and load a new page. Event handlers can prevent this default action by invoking a method of the event object. This is sometimes called “canceling” the event and is covered in §15.2.5. Some events have default actions associated with them. When a click event occurs on a hyperlink, for example, the default action is for the browser to follow the link and load a new page. Event handlers can prevent this default action by invoking a method of the event object. This is sometimes called “canceling” the event and is covered in §15.2.5.
15.2.1 Event Categories ### 15.2.1 Event Categories
Client-side JavaScript supports such a large number of event types that there is no way this chapter can cover them all. It can be useful, though, to group events into some general categories, to illustrate the scope and wide variety of supported events: Client-side JavaScript supports such a large number of event types that there is no way this chapter can cover them all. It can be useful, though, to group events into some general categories, to illustrate the scope and wide variety of supported events:
Device-dependent input events Device-dependent input events
...@@ -349,7 +349,7 @@ Some events are not triggered directly by user activity, but by network or brows ...@@ -349,7 +349,7 @@ Some events are not triggered directly by user activity, but by network or brows
API-specific events API-specific events
A number of web APIs defined by HTML and related specifications include their own event types. The HTML <video> and <audio> elements define a long list of associated event types such as “waiting,” “playing,” “seeking,” “volumechange,” and so on, and you can use them to customize media playback. Generally speaking, web platform APIs that are asynchronous and were developed before Promises were added to JavaScript are event-based and define API-specific events. The IndexedDB API, for example (§15.12.3), fires “success” and “error” events when database requests succeed or fail. And although the new fetch() API (§15.11.1) for making HTTP requests is Promise-based, the XMLHttpRequest API that it replaces defines a number of API-specific event types. A number of web APIs defined by HTML and related specifications include their own event types. The HTML <video> and <audio> elements define a long list of associated event types such as “waiting,” “playing,” “seeking,” “volumechange,” and so on, and you can use them to customize media playback. Generally speaking, web platform APIs that are asynchronous and were developed before Promises were added to JavaScript are event-based and define API-specific events. The IndexedDB API, for example (§15.12.3), fires “success” and “error” events when database requests succeed or fail. And although the new fetch() API (§15.11.1) for making HTTP requests is Promise-based, the XMLHttpRequest API that it replaces defines a number of API-specific event types.
15.2.2 Registering Event Handlers ### 15.2.2 Registering Event Handlers
There are two basic ways to register event handlers. The first, from the early days of the web, is to set a property on the object or document element that is the event target. The second (newer and more general) technique is to pass the handler to the addEventListener() method of the object or element. There are two basic ways to register event handlers. The first, from the early days of the web, is to set a property on the object or document element that is the event target. The second (newer and more general) technique is to pass the handler to the addEventListener() method of the object or element.
SETTING EVENT HANDLER PROPERTIES SETTING EVENT HANDLER PROPERTIES
...@@ -425,7 +425,7 @@ If the Options object has a passive property set to true, it indicates that the ...@@ -425,7 +425,7 @@ If the Options object has a passive property set to true, it indicates that the
You can also pass an Options object to removeEventListener(), but the capture property is the only one that is relevant. There is no need to specify once or passive when removing a listener, and these properties are ignored. You can also pass an Options object to removeEventListener(), but the capture property is the only one that is relevant. There is no need to specify once or passive when removing a listener, and these properties are ignored.
15.2.3 Event Handler Invocation ### 15.2.3 Event Handler Invocation
Once you’ve registered an event handler, the web browser will invoke it automatically when an event of the specified type occurs on the specified object. This section describes event handler invocation in detail, explaining event handler arguments, the invocation context (the this value), and the meaning of the return value of an event handler. Once you’ve registered an event handler, the web browser will invoke it automatically when an event of the specified type occurs on the specified object. This section describes event handler invocation in detail, explaining event handler arguments, the invocation context (the this value), and the meaning of the return value of an event handler.
EVENT HANDLER ARGUMENT EVENT HANDLER ARGUMENT
...@@ -464,7 +464,7 @@ The standard and preferred way to prevent the browser from performing a default ...@@ -464,7 +464,7 @@ The standard and preferred way to prevent the browser from performing a default
INVOCATION ORDER INVOCATION ORDER
An event target may have more than one event handler registered for a particular type of event. When an event of that type occurs, the browser invokes all of the handlers in the order in which they were registered. Interestingly, this is true even if you mix event handlers registered with addEventListener() with an event handler registered on an object property like onclick. An event target may have more than one event handler registered for a particular type of event. When an event of that type occurs, the browser invokes all of the handlers in the order in which they were registered. Interestingly, this is true even if you mix event handlers registered with addEventListener() with an event handler registered on an object property like onclick.
15.2.4 Event Propagation ### 15.2.4 Event Propagation
When the target of an event is the Window object or some other standalone object, the browser responds to an event simply by invoking the appropriate handlers on that one object. When the event target is a Document or document Element, however, the situation is more complicated. When the target of an event is the Window object or some other standalone object, the browser responds to an event simply by invoking the appropriate handlers on that one object. When the event target is a Document or document Element, however, the situation is more complicated.
After the event handlers registered on the target element are invoked, most events “bubble” up the DOM tree. The event handlers of the target’s parent are invoked. Then the handlers registered on the target’s grandparent are invoked. This continues up to the Document object, and then beyond to the Window object. Event bubbling provides an alternative to registering handlers on lots of individual document elements: instead, you can register a single handler on a common ancestor element and handle events there. You might register a “change” handler on a <form> element, for example, instead of registering a “change” handler for every element in the form. After the event handlers registered on the target element are invoked, most events “bubble” up the DOM tree. The event handlers of the target’s parent are invoked. Then the handlers registered on the target’s grandparent are invoked. This continues up to the Document object, and then beyond to the Window object. Event bubbling provides an alternative to registering handlers on lots of individual document elements: instead, you can register a single handler on a common ancestor element and handle events there. You might register a “change” handler on a <form> element, for example, instead of registering a “change” handler for every element in the form.
...@@ -475,12 +475,12 @@ Event bubbling is the third “phase” of event propagation. The invocation of ...@@ -475,12 +475,12 @@ Event bubbling is the third “phase” of event propagation. The invocation of
Event capturing provides an opportunity to peek at events before they are delivered to their target. A capturing event handler can be used for debugging, or it can be used along with the event cancellation technique described in the next section to filter events so that the target event handlers are never actually invoked. One common use for event capturing is handling mouse drags, where mouse motion events need to be handled by the object being dragged, not the document elements over which it is dragged. Event capturing provides an opportunity to peek at events before they are delivered to their target. A capturing event handler can be used for debugging, or it can be used along with the event cancellation technique described in the next section to filter events so that the target event handlers are never actually invoked. One common use for event capturing is handling mouse drags, where mouse motion events need to be handled by the object being dragged, not the document elements over which it is dragged.
15.2.5 Event Cancellation ### 15.2.5 Event Cancellation
Browsers respond to many user events, even if your code does not: when the user clicks the mouse on a hyperlink, the browser follows the link. If an HTML text input element has the keyboard focus and the user types a key, the browser will enter the user’s input. If the user moves their finger across a touch-screen device, the browser scrolls. If you register an event handler for events like these, you can prevent the browser from performing its default action by invoking the preventDefault() method of the event object. (Unless you registered the handler with the passive option, which makes preventDefault() ineffective.) Browsers respond to many user events, even if your code does not: when the user clicks the mouse on a hyperlink, the browser follows the link. If an HTML text input element has the keyboard focus and the user types a key, the browser will enter the user’s input. If the user moves their finger across a touch-screen device, the browser scrolls. If you register an event handler for events like these, you can prevent the browser from performing its default action by invoking the preventDefault() method of the event object. (Unless you registered the handler with the passive option, which makes preventDefault() ineffective.)
Canceling the default action associated with an event is only one kind of event cancellation. We can also cancel the propagation of events by calling the stopPropagation() method of the event object. If there are other handlers defined on the same object, the rest of those handlers will still be invoked, but no event handlers on any other object will be invoked after stopPropagation() is called. stopPropagation() works during the capturing phase, at the event target itself, and during the bubbling phase. stopImmediatePropagation() works like stopPropagation(), but it also prevents the invocation of any subsequent event handlers registered on the same object. Canceling the default action associated with an event is only one kind of event cancellation. We can also cancel the propagation of events by calling the stopPropagation() method of the event object. If there are other handlers defined on the same object, the rest of those handlers will still be invoked, but no event handlers on any other object will be invoked after stopPropagation() is called. stopPropagation() works during the capturing phase, at the event target itself, and during the bubbling phase. stopImmediatePropagation() works like stopPropagation(), but it also prevents the invocation of any subsequent event handlers registered on the same object.
15.2.6 Dispatching Custom Events ### 15.2.6 Dispatching Custom Events
Client-side JavaScript’s event API is a relatively powerful one, and you can use it to define and dispatch your own events. Suppose, for example, that your program periodically needs to perform a long calculation or make a network request and that, while this operation is pending, other operations are not possible. You want to let the user know about this by displaying “spinners” to indicate that the application is busy. But the module that is busy should not need to know where the spinners should be displayed. Instead, that module might just dispatch an event to announce that it is busy and then dispatch another event when it is no longer busy. Then, the UI module can register event handlers for those events and take whatever UI actions are appropriate to notify the user. Client-side JavaScript’s event API is a relatively powerful one, and you can use it to define and dispatch your own events. Suppose, for example, that your program periodically needs to perform a long calculation or make a network request and that, while this operation is pending, other operations are not possible. You want to let the user know about this by displaying “spinners” to indicate that the application is busy. But the module that is busy should not need to know where the spinners should be displayed. Instead, that module might just dispatch an event to announce that it is busy and then dispatch another event when it is no longer busy. Then, the UI module can register event handlers for those events and take whatever UI actions are appropriate to notify the user.
If a JavaScript object has an addEventListener() method, then it is an “event target,” and this means it also has a dispatchEvent() method. You can create your own event object with the CustomEvent() constructor and pass it to dispatchEvent(). The first argument to CustomEvent() is a string that specifies the type of your event, and the second argument is an object that specifies the properties of the event object. Set the detail property of this object to a string, object, or other value that represents the content of your event. If you plan to dispatch your event on a document element and want it to bubble up the document tree, add bubbles:true to the second argument: If a JavaScript object has an addEventListener() method, then it is an “event target,” and this means it also has a dispatchEvent() method. You can create your own event object with the CustomEvent() constructor and pass it to dispatchEvent(). The first argument to CustomEvent() is a string that specifies the type of your event, and the second argument is an object that specifies the properties of the event object. Set the detail property of this object to a string, object, or other value that represents the content of your event. If you plan to dispatch your event on a document element and want it to bubble up the document tree, add bubbles:true to the second argument:
...@@ -507,7 +507,7 @@ document.addEventListener("busy", (e) => { ...@@ -507,7 +507,7 @@ document.addEventListener("busy", (e) => {
hideSpinner(); hideSpinner();
} }
}); });
15.3 Scripting Documents ## 15.3 Scripting Documents
Client-side JavaScript exists to turn static HTML documents into interactive web applications. So scripting the content of web pages is really the central purpose of JavaScript. Client-side JavaScript exists to turn static HTML documents into interactive web applications. So scripting the content of web pages is really the central purpose of JavaScript.
Every Window object has a document property that refers to a Document object. The Document object represents the content of the window, and it is the subject of this section. The Document object does not stand alone, however. It is the central object in the DOM for representing and manipulating document content. Every Window object has a document property that refers to a Document object. The Document object represents the content of the window, and it is the subject of this section. The Document object does not stand alone, however. It is the central object in the DOM for representing and manipulating document content.
...@@ -524,7 +524,7 @@ How to query, set, and modify the content of a document. ...@@ -524,7 +524,7 @@ How to query, set, and modify the content of a document.
How to modify the structure of a document by creating, inserting, and deleting nodes. How to modify the structure of a document by creating, inserting, and deleting nodes.
15.3.1 Selecting Document Elements ### 15.3.1 Selecting Document Elements
Client-side JavaScript programs often need to manipulate one or more elements within the document. The global document property refers to the Document object, and the Document object has head and body properties that refer to the Element objects for the <head> and <body> tags, respectively. But a program that wants to manipulate an element embedded more deeply in the document must somehow obtain or select the Element objects that refer to those document elements. Client-side JavaScript programs often need to manipulate one or more elements within the document. The global document property refers to the Document object, and the Document object has head and body properties that refer to the Element objects for the <head> and <body> tags, respectively. But a program that wants to manipulate an element embedded more deeply in the document must somehow obtain or select the Element objects that refer to those document elements.
SELECTING ELEMENTS WITH CSS SELECTORS SELECTING ELEMENTS WITH CSS SELECTORS
...@@ -619,7 +619,7 @@ For historical reasons, the Document class defines shortcut properties to access ...@@ -619,7 +619,7 @@ For historical reasons, the Document class defines shortcut properties to access
document.forms.address; document.forms.address;
An even more outdated API for selecting elements is the document.all property, which is like an HTMLCollection for all elements in the document. document.all is deprecated, and you should no longer use it. An even more outdated API for selecting elements is the document.all property, which is like an HTMLCollection for all elements in the document. document.all is deprecated, and you should no longer use it.
15.3.2 Document Structure and Traversal ### 15.3.2 Document Structure and Traversal
Once you have selected an Element from a Document, you sometimes need to find structurally related portions (parent, siblings, children) of the document. When we are primarily interested in the Elements of a document instead of the text within them (and the whitespace between them, which is also text), there is a traversal API that allows us to treat a document as a tree of Element objects, ignoring Text nodes that are also part of the document. This traversal API does not involve any methods; it is simply a set of properties on Element objects that allow us to refer to the parent, children, and siblings of a given element: Once you have selected an Element from a Document, you sometimes need to find structurally related portions (parent, siblings, children) of the document. When we are primarily interested in the Elements of a document instead of the text within them (and the whitespace between them, which is also text), there is a traversal API that allows us to treat a document as a tree of Element objects, ignoring Text nodes that are also part of the document. This traversal API does not involve any methods; it is simply a set of properties on Element objects that allow us to refer to the parent, children, and siblings of a given element:
parentNode parentNode
...@@ -717,7 +717,7 @@ function textContent(e) { ...@@ -717,7 +717,7 @@ function textContent(e) {
} }
This function is a demonstration only—in practice, you would simply write e.textContent to obtain the textual content of the element e. This function is a demonstration only—in practice, you would simply write e.textContent to obtain the textual content of the element e.
15.3.3 Attributes ### 15.3.3 Attributes
HTML elements consist of a tag name and a set of name/value pairs known as attributes. The <a> element that defines a hyperlink, for example, uses the value of its href attribute as the destination of the link. HTML elements consist of a tag name and a set of name/value pairs known as attributes. The <a> element that defines a hyperlink, for example, uses the value of its href attribute as the destination of the link.
The Element class defines general getAttribute(), setAttribute(), hasAttribute(), and removeAttribute() methods for querying, setting, testing, and removing the attributes of an element. But the attribute values of HTML elements (for all standard attributes of standard HTML elements) are available as properties of the HTMLElement objects that represent those elements, and it is usually much easier to work with them as JavaScript properties than it is to call getAttribute() and related methods. The Element class defines general getAttribute(), setAttribute(), hasAttribute(), and removeAttribute() methods for querying, setting, testing, and removing the attributes of an element. But the attribute values of HTML elements (for all standard attributes of standard HTML elements) are available as properties of the HTMLElement objects that represent those elements, and it is usually much easier to work with them as JavaScript properties than it is to call getAttribute() and related methods.
...@@ -765,7 +765,7 @@ Suppose an HTML document contains this text: ...@@ -765,7 +765,7 @@ Suppose an HTML document contains this text:
Then you could write JavaScript like this to access that section number: Then you could write JavaScript like this to access that section number:
let number = document.querySelector("#title").dataset.sectionNumber; let number = document.querySelector("#title").dataset.sectionNumber;
15.3.4 Element Content ### 15.3.4 Element Content
Look again at the document tree pictured in Figure 15-1, and ask yourself what the “content” of the <p> element is. There are two ways we might answer this question: Look again at the document tree pictured in Figure 15-1, and ask yourself what the “content” of the <p> element is. There are two ways we might answer this question:
The content is the HTML string “This is a <i>simple</i> document”. The content is the HTML string “This is a <i>simple</i> document”.
...@@ -802,7 +802,7 @@ The Element class defines an innerText property that is similar to textContent. ...@@ -802,7 +802,7 @@ The Element class defines an innerText property that is similar to textContent.
TEXT IN <SCRIPT> ELEMENTS TEXT IN <SCRIPT> ELEMENTS
Inline <script> elements (i.e., those that do not have a src attribute) have a text property that you can use to retrieve their text. The content of a <script> element is never displayed by the browser, and the HTML parser ignores angle brackets and ampersands within a script. This makes a <script> element an ideal place to embed arbitrary textual data for use by your application. Simply set the type attribute of the element to some value (such as “text/x-custom-data”) that makes it clear that the script is not executable JavaScript code. If you do this, the JavaScript interpreter will ignore the script, but the element will exist in the document tree, and its text property will return the data to you. Inline <script> elements (i.e., those that do not have a src attribute) have a text property that you can use to retrieve their text. The content of a <script> element is never displayed by the browser, and the HTML parser ignores angle brackets and ampersands within a script. This makes a <script> element an ideal place to embed arbitrary textual data for use by your application. Simply set the type attribute of the element to some value (such as “text/x-custom-data”) that makes it clear that the script is not executable JavaScript code. If you do this, the JavaScript interpreter will ignore the script, but the element will exist in the document tree, and its text property will return the data to you.
15.3.5 Creating, Inserting, and Deleting Nodes ### 15.3.5 Creating, Inserting, and Deleting Nodes
We’ve seen how to query and alter document content using strings of HTML and of plain text. And we’ve also seen that we can traverse a Document to examine the individual Element and Text nodes that it is made of. It is also possible to alter a document at the level of individual nodes. The Document class defines methods for creating Element objects, and Element and Text objects have methods for inserting, deleting, and replacing nodes in the tree. We’ve seen how to query and alter document content using strings of HTML and of plain text. And we’ve also seen that we can traverse a Document to examine the individual Element and Text nodes that it is made of. It is also possible to alter a document at the level of individual nodes. The Document class defines methods for creating Element objects, and Element and Text objects have methods for inserting, deleting, and replacing nodes in the tree.
Create a new element with the createElement() method of the Document class and append strings of text or other elements to it with its append() and prepend() methods: Create a new element with the createElement() method of the Document class and append strings of text or other elements to it with its append() and prepend() methods:
...@@ -844,7 +844,7 @@ greetings.replaceWith(paragraph); ...@@ -844,7 +844,7 @@ greetings.replaceWith(paragraph);
paragraph.remove(); paragraph.remove();
The DOM API also defines an older generation of methods for inserting and removing content. appendChild(), insertBefore(), replaceChild(), and removeChild() are harder to use than the methods shown here and should never be needed. The DOM API also defines an older generation of methods for inserting and removing content. appendChild(), insertBefore(), replaceChild(), and removeChild() are harder to use than the methods shown here and should never be needed.
15.3.6 Example: Generating a Table of Contents ### 15.3.6 Example: Generating a Table of Contents
Example 15-1 shows how to dynamically create a table of contents for a document. It demonstrates many of the document scripting techniques described in the previous sections. The example is well commented, and you should have no trouble following the code. Example 15-1 shows how to dynamically create a table of contents for a document. It demonstrates many of the document scripting techniques described in the previous sections. The example is well commented, and you should have no trouble following the code.
Example 15-1. Generating a table of contents with the DOM API Example 15-1. Generating a table of contents with the DOM API
...@@ -958,7 +958,7 @@ document.addEventListener("DOMContentLoaded", () => { ...@@ -958,7 +958,7 @@ document.addEventListener("DOMContentLoaded", () => {
toc.append(entry); toc.append(entry);
} }
}); });
15.4 Scripting CSS ## 15.4 Scripting CSS
We’ve seen that JavaScript can control the logical structure and content of HTML documents. It can also control the visual appearance and layout of those documents by scripting CSS. The following subsections explain a few different techniques that JavaScript code can use to work with CSS. We’ve seen that JavaScript can control the logical structure and content of HTML documents. It can also control the visual appearance and layout of those documents by scripting CSS. The following subsections explain a few different techniques that JavaScript code can use to work with CSS.
This is a book about JavaScript, not about CSS, and this section assumes that you already have a working knowledge of how CSS is used to style HTML content. But it’s worth mentioning some of the CSS styles that are commonly scripted from JavaScript: This is a book about JavaScript, not about CSS, and this section assumes that you already have a working knowledge of how CSS is used to style HTML content. But it’s worth mentioning some of the CSS styles that are commonly scripted from JavaScript:
...@@ -971,7 +971,7 @@ You can shift, scale, and rotate elements with the transform style. ...@@ -971,7 +971,7 @@ You can shift, scale, and rotate elements with the transform style.
You can animate changes to other CSS styles with the transition style. These animations are handled automatically by the web browser and do not require JavaScript, but you can use JavaScript to initiate the animations. You can animate changes to other CSS styles with the transition style. These animations are handled automatically by the web browser and do not require JavaScript, but you can use JavaScript to initiate the animations.
15.4.1 CSS Classes ### 15.4.1 CSS Classes
The simplest way to use JavaScript to affect the styling of document content is to add and remove CSS class names from the class attribute of HTML tags. This is easy to do with the classList property of Element objects, as explained in “The class attribute”. The simplest way to use JavaScript to affect the styling of document content is to add and remove CSS class names from the class attribute of HTML tags. This is easy to do with the classList property of Element objects, as explained in “The class attribute”.
Suppose, for example, that your document’s stylesheet includes a definition for a “hidden” class: Suppose, for example, that your document’s stylesheet includes a definition for a “hidden” class:
...@@ -987,7 +987,7 @@ document.querySelector("#tooltip").classList.remove("hidden"); ...@@ -987,7 +987,7 @@ document.querySelector("#tooltip").classList.remove("hidden");
// And we can hide it again like this: // And we can hide it again like this:
document.querySelector("#tooltip").classList.add("hidden"); document.querySelector("#tooltip").classList.add("hidden");
15.4.2 Inline Styles ### 15.4.2 Inline Styles
To continue with the preceding tooltip example, suppose that the document is structured with only a single tooltip element, and we want to dynamically position it before displaying it. In general, we can’t create a different stylesheet class for each possible position of the tooltip, so the classList property won’t help us with positioning. To continue with the preceding tooltip example, suppose that the document is structured with only a single tooltip element, and we want to dynamically position it before displaying it. In general, we can’t create a different stylesheet class for each possible position of the tooltip, so the classList property won’t help us with positioning.
In this case, we need to script the style attribute of the tooltip element to set inline styles that are specific to that one element. The DOM defines a style property on all Element objects that correspond to the style attribute. Unlike most such properties, however, the style property is not a string. Instead, it is a CSSStyleDeclaration object: a parsed representation of the CSS styles that appear in textual form in the style attribute. To display and set the position of our hypothetical tooltip with JavaScript, we might use code like this: In this case, we need to script the style attribute of the tooltip element to set inline styles that are specific to that one element. The DOM defines a style property on all Element objects that correspond to the style attribute. Unlike most such properties, however, the style property is not a string. Instead, it is a CSSStyleDeclaration object: a parsed representation of the CSS styles that appear in textual form in the style attribute. To display and set the position of our hypothetical tooltip with JavaScript, we might use code like this:
...@@ -1033,7 +1033,7 @@ f.setAttribute("style", e.getAttribute("style")); ...@@ -1033,7 +1033,7 @@ f.setAttribute("style", e.getAttribute("style"));
f.style.cssText = e.style.cssText; f.style.cssText = e.style.cssText;
When querying the style property of an element, keep in mind that it represents only the inline styles of an element and that most styles for most elements are specified in stylesheets rather than inline. Furthermore, the values you obtain when querying the style property will use whatever units and whatever shortcut property format is actually used on the HTML attribute, and your code may have to do some sophisticated parsing to interpret them. In general, if you want to query the styles of an element, you probably want the computed style, which is discussed next. When querying the style property of an element, keep in mind that it represents only the inline styles of an element and that most styles for most elements are specified in stylesheets rather than inline. Furthermore, the values you obtain when querying the style property will use whatever units and whatever shortcut property format is actually used on the HTML attribute, and your code may have to do some sophisticated parsing to interpret them. In general, if you want to query the styles of an element, you probably want the computed style, which is discussed next.
15.4.3 Computed Styles ### 15.4.3 Computed Styles
The computed style for an element is the set of property values that the browser derives (or computes) from the element’s inline style plus all applicable style rules in all stylesheets: it is the set of properties actually used to display the element. Like inline styles, computed styles are represented with a CSSStyleDeclaration object. Unlike inline styles, however, computed styles are read-only. You can’t set these styles, but the computed CSSStyleDeclaration object for an element lets you determine what style property values the browser used when rendering that element. The computed style for an element is the set of property values that the browser derives (or computes) from the element’s inline style plus all applicable style rules in all stylesheets: it is the set of properties actually used to display the element. Like inline styles, computed styles are represented with a CSSStyleDeclaration object. Unlike inline styles, however, computed styles are read-only. You can’t set these styles, but the computed CSSStyleDeclaration object for an element lets you determine what style property values the browser used when rendering that element.
Obtain the computed style for an element with the getComputedStyle() method of the Window object. The first argument to this method is the element whose computed style is desired. The optional second argument is used to specify a CSS pseudoelement, such as “::before” or “::after”: Obtain the computed style for an element with the getComputedStyle() method of the Window object. The first argument to this method is the element whose computed style is desired. The optional second argument is used to specify a CSS pseudoelement, such as “::before” or “::after”:
...@@ -1055,7 +1055,7 @@ A CSSStyleDeclaration object returned by getComputedStyle() generally contains m ...@@ -1055,7 +1055,7 @@ A CSSStyleDeclaration object returned by getComputedStyle() generally contains m
Although CSS can be used to precisely specify the position and size of document elements, querying the computed style of an element is not the preferred way to determine the element’s size and position. See §15.5.2 for a simpler, portable alternative. Although CSS can be used to precisely specify the position and size of document elements, querying the computed style of an element is not the preferred way to determine the element’s size and position. See §15.5.2 for a simpler, portable alternative.
15.4.4 Scripting Stylesheets ### 15.4.4 Scripting Stylesheets
In addition to scripting class attributes and inline styles, JavaScript can also manipulate stylesheets themselves. Stylesheets are associated with an HTML document with a <style> tag or with a <link rel="stylesheet"> tag. Both of these are regular HTML tags, so you can give them both id attributes and then look them up with document.querySelector(). In addition to scripting class attributes and inline styles, JavaScript can also manipulate stylesheets themselves. Stylesheets are associated with an HTML document with a <style> tag or with a <link rel="stylesheet"> tag. Both of these are regular HTML tags, so you can give them both id attributes and then look them up with document.querySelector().
The Element objects for both <style> and <link> tags have a disabled property that you can use to disable the entire stylesheet. You might use it with code like this: The Element objects for both <style> and <link> tags have a disabled property that you can use to disable the entire stylesheet. You might use it with code like this:
...@@ -1099,7 +1099,7 @@ document.head.insertAdjacentHTML( ...@@ -1099,7 +1099,7 @@ document.head.insertAdjacentHTML(
); );
Browsers define an API that allows JavaScript to look inside stylesheets to query, modify, insert, and delete style rules in that stylesheet. This API is so specialized that it is not documented here. You can read about it on MDN by searching for “CSSStyleSheet” and “CSS Object Model.” Browsers define an API that allows JavaScript to look inside stylesheets to query, modify, insert, and delete style rules in that stylesheet. This API is so specialized that it is not documented here. You can read about it on MDN by searching for “CSSStyleSheet” and “CSS Object Model.”
15.4.5 CSS Animations and Events ### 15.4.5 CSS Animations and Events
Suppose you have the following two CSS classes defined in a stylesheet: Suppose you have the following two CSS classes defined in a stylesheet:
.transparent { opacity: 0; } .transparent { opacity: 0; }
...@@ -1124,12 +1124,12 @@ In addition to transitions, CSS also supports a more complex form of animation k ...@@ -1124,12 +1124,12 @@ In addition to transitions, CSS also supports a more complex form of animation k
And like CSS transitions, CSS animations also trigger events that your JavaScript code can listen form. “animationstart” is dispatched when the animation starts, and “animationend” is dispatched when it is complete. If the animation repeats more than once, then an “animationiteration” event is dispatched after each repetition except the last. The event target is the animated element, and the event object passed to handler functions is an AnimationEvent object. These events include an animationName property that specifies the animation-name property that defines the animation and an elapsedTime property that specifies how many seconds have passed since the animation started. And like CSS transitions, CSS animations also trigger events that your JavaScript code can listen form. “animationstart” is dispatched when the animation starts, and “animationend” is dispatched when it is complete. If the animation repeats more than once, then an “animationiteration” event is dispatched after each repetition except the last. The event target is the animated element, and the event object passed to handler functions is an AnimationEvent object. These events include an animationName property that specifies the animation-name property that defines the animation and an elapsedTime property that specifies how many seconds have passed since the animation started.
15.5 Document Geometry and Scrolling ## 15.5 Document Geometry and Scrolling
In this chapter so far, we have thought about documents as abstract trees of elements and text nodes. But when a browser renders a document within a window, it creates a visual representation of the document in which each element has a position and a size. Often, web applications can treat documents as trees of elements and never have to think about how those elements are rendered on screen. Sometimes, however, it is necessary to determine the precise geometry of an element. If, for example, you want to use CSS to dynamically position an element (such as a tooltip) next to some ordinary browser-positioned element, you need to be able to determine the location of that element. In this chapter so far, we have thought about documents as abstract trees of elements and text nodes. But when a browser renders a document within a window, it creates a visual representation of the document in which each element has a position and a size. Often, web applications can treat documents as trees of elements and never have to think about how those elements are rendered on screen. Sometimes, however, it is necessary to determine the precise geometry of an element. If, for example, you want to use CSS to dynamically position an element (such as a tooltip) next to some ordinary browser-positioned element, you need to be able to determine the location of that element.
The following subsections explain how you can go back and forth between the abstract, tree-based model of a document and the geometrical, coordinate-based view of the document as it is laid out in a browser window. The following subsections explain how you can go back and forth between the abstract, tree-based model of a document and the geometrical, coordinate-based view of the document as it is laid out in a browser window.
15.5.1 Document Coordinates and Viewport Coordinates ### 15.5.1 Document Coordinates and Viewport Coordinates
The position of a document element is measured in CSS pixels, with the x coordinate increasing to the right and the y coordinate increasing as we go down. There are two different points we can use as the coordinate system origin, however: the x and y coordinates of an element can be relative to the top-left corner of the document or relative to the top-left corner of the viewport in which the document is displayed. In top-level windows and tabs, the “viewport” is the portion of the browser that actually displays document content: it excludes browser “chrome” such as menus, toolbars, and tabs. For documents displayed in <iframe> tags, it is the iframe element in the DOM that defines the viewport for the nested document. In either case, when we talk about the position of an element, we must be clear whether we are using document coordinates or viewport coordinates. (Note that viewport coordinates are sometimes called “window coordinates.”) The position of a document element is measured in CSS pixels, with the x coordinate increasing to the right and the y coordinate increasing as we go down. There are two different points we can use as the coordinate system origin, however: the x and y coordinates of an element can be relative to the top-left corner of the document or relative to the top-left corner of the viewport in which the document is displayed. In top-level windows and tabs, the “viewport” is the portion of the browser that actually displays document content: it excludes browser “chrome” such as menus, toolbars, and tabs. For documents displayed in <iframe> tags, it is the iframe element in the DOM that defines the viewport for the nested document. In either case, when we talk about the position of an element, we must be clear whether we are using document coordinates or viewport coordinates. (Note that viewport coordinates are sometimes called “window coordinates.”)
If the document is smaller than the viewport, or if it has not been scrolled, the upper-left corner of the document is in the upper-left corner of the viewport and the document and viewport coordinate systems are the same. In general, however, to convert between the two coordinate systems, we must add or subtract the scroll offsets. If an element has a y coordinate of 200 pixels in document coordinates, for example, and if the user has scrolled down by 75 pixels, then that element has a y coordinate of 125 pixels in viewport coordinates. Similarly, if an element has an x coordinate of 400 in viewport coordinates after the user has scrolled the viewport 200 pixels horizontally, then the element’s x coordinate in document coordinates is 600. If the document is smaller than the viewport, or if it has not been scrolled, the upper-left corner of the document is in the upper-left corner of the viewport and the document and viewport coordinate systems are the same. In general, however, to convert between the two coordinate systems, we must add or subtract the scroll offsets. If an element has a y coordinate of 200 pixels in document coordinates, for example, and if the user has scrolled down by 75 pixels, then that element has a y coordinate of 125 pixels in viewport coordinates. Similarly, if an element has an x coordinate of 400 in viewport coordinates after the user has scrolled the viewport 200 pixels horizontally, then the element’s x coordinate in document coordinates is 600.
...@@ -1145,15 +1145,15 @@ If, like me, you are old enough to remember computer monitors with resolutions o ...@@ -1145,15 +1145,15 @@ If, like me, you are old enough to remember computer monitors with resolutions o
devicePixelRatio does not have to be an integer. If you are using a CSS font size of “12px” and the device pixel ratio is 2.5, then the actual font size, in device pixels, is 30. Because the pixel values we use in CSS no longer correspond directly to individual pixels on the screen, pixel coordinates no longer need to be integers. If the devicePixelRatio is 3, then a coordinate of 3.33 makes perfect sense. And if the ratio is actually 2, then a coordinate of 3.33 will just be rounded up to 3.5. devicePixelRatio does not have to be an integer. If you are using a CSS font size of “12px” and the device pixel ratio is 2.5, then the actual font size, in device pixels, is 30. Because the pixel values we use in CSS no longer correspond directly to individual pixels on the screen, pixel coordinates no longer need to be integers. If the devicePixelRatio is 3, then a coordinate of 3.33 makes perfect sense. And if the ratio is actually 2, then a coordinate of 3.33 will just be rounded up to 3.5.
15.5.2 Querying the Geometry of an Element ### 15.5.2 Querying the Geometry of an Element
You can determine the size (including CSS border and padding, but not the margin) and position (in viewport coordinates) of an element by calling its getBoundingClientRect() method. It takes no arguments and returns an object with properties left, right, top, bottom, width, and height. The left and top properties give the x and y coordinates of the upper-left corner of the element, and the right and bottom properties give the coordinates of the lower-right corner. The differences between these values are the width and height properties. You can determine the size (including CSS border and padding, but not the margin) and position (in viewport coordinates) of an element by calling its getBoundingClientRect() method. It takes no arguments and returns an object with properties left, right, top, bottom, width, and height. The left and top properties give the x and y coordinates of the upper-left corner of the element, and the right and bottom properties give the coordinates of the lower-right corner. The differences between these values are the width and height properties.
Block elements, such as images, paragraphs, and <div> elements are always rectangular when laid out by the browser. Inline elements, such as <span>, <code>, and <b> elements, however, may span multiple lines and may therefore consist of multiple rectangles. Imagine, for example, some text within <em> and </em> tags that happens to be displayed so that it wraps across two lines. Its rectangles consist of the end of the first line and beginning of the second line. If you call getBoundingClientRect() on this element, the bounding rectangle would include the entire width of both lines. If you want to query the individual rectangles of inline elements, call the getClientRects() method to obtain a read-only, array-like object whose elements are rectangle objects like those returned by getBoundingClientRect(). Block elements, such as images, paragraphs, and <div> elements are always rectangular when laid out by the browser. Inline elements, such as <span>, <code>, and <b> elements, however, may span multiple lines and may therefore consist of multiple rectangles. Imagine, for example, some text within <em> and </em> tags that happens to be displayed so that it wraps across two lines. Its rectangles consist of the end of the first line and beginning of the second line. If you call getBoundingClientRect() on this element, the bounding rectangle would include the entire width of both lines. If you want to query the individual rectangles of inline elements, call the getClientRects() method to obtain a read-only, array-like object whose elements are rectangle objects like those returned by getBoundingClientRect().
15.5.3 Determining the Element at a Point ### 15.5.3 Determining the Element at a Point
The getBoundingClientRect() method allows us to determine the current position of an element in a viewport. Sometimes we want to go in the other direction and determine which element is at a given location in the viewport. You can determine this with the elementFromPoint() method of the Document object. Call this method with the x and y coordinates of a point (using viewport coordinates, not document coordinates: the clientX and clientY coordinates of a mouse event work, for example). elementFromPoint() returns an Element object that is at the specified position. The hit detection algorithm for selecting the element is not precisely specified, but the intent of this method is that it returns the innermost (most deeply nested) and uppermost (highest CSS z-index attribute) element at that point. The getBoundingClientRect() method allows us to determine the current position of an element in a viewport. Sometimes we want to go in the other direction and determine which element is at a given location in the viewport. You can determine this with the elementFromPoint() method of the Document object. Call this method with the x and y coordinates of a point (using viewport coordinates, not document coordinates: the clientX and clientY coordinates of a mouse event work, for example). elementFromPoint() returns an Element object that is at the specified position. The hit detection algorithm for selecting the element is not precisely specified, but the intent of this method is that it returns the innermost (most deeply nested) and uppermost (highest CSS z-index attribute) element at that point.
15.5.4 Scrolling ### 15.5.4 Scrolling
The scrollTo() method of the Window object takes the x and y coordinates of a point (in document coordinates) and sets these as the scrollbar offsets. That is, it scrolls the window so that the specified point is in the upper-left corner of the viewport. If you specify a point that is too close to the bottom or too close to the right edge of the document, the browser will move it as close as possible to the upper-left corner but won’t be able to get it all the way there. The following code scrolls the browser so that the bottom-most page of the document is visible: The scrollTo() method of the Window object takes the x and y coordinates of a point (in document coordinates) and sets these as the scrollbar offsets. That is, it scrolls the window so that the specified point is in the upper-left corner of the viewport. If you specify a point that is too close to the bottom or too close to the right edge of the document, the browser will move it as close as possible to the upper-left corner but won’t be able to get it all the way there. The following code scrolls the browser so that the bottom-most page of the document is visible:
// Get the heights of the document and viewport. // Get the heights of the document and viewport.
...@@ -1176,7 +1176,7 @@ Often, instead of scrolling to a numeric location in a document, we just want to ...@@ -1176,7 +1176,7 @@ Often, instead of scrolling to a numeric location in a document, we just want to
You can also pass an object to scrollIntoView(), setting the behavior:"smooth" property for smooth scrolling. You can set the block property to specify where the element should be positioned vertically and the inline property to specify how it should be positioned horizontally if horizontal scrolling is needed. Legal values for both of these properties are start, end, nearest, and center. You can also pass an object to scrollIntoView(), setting the behavior:"smooth" property for smooth scrolling. You can set the block property to specify where the element should be positioned vertically and the inline property to specify how it should be positioned horizontally if horizontal scrolling is needed. Legal values for both of these properties are start, end, nearest, and center.
15.5.5 Viewport Size, Content Size, and Scroll Position ### 15.5.5 Viewport Size, Content Size, and Scroll Position
As we’ve discussed, browser windows and other HTML elements can display scrolling content. When this is the case, we sometimes need to know the size of the viewport, the size of the content, and the scroll offsets of the content within the viewport. This section covers these details. As we’ve discussed, browser windows and other HTML elements can display scrolling content. When this is the case, we sometimes need to know the size of the viewport, the size of the content, and the scroll offsets of the content within the viewport. This section covers these details.
For browser windows, the viewport size is given by the window.innerWidth and window.innerHeight properties. (Web pages optimized for mobile devices often use a <meta name="viewport"> tag in their <head> to set the desired viewport width for the page.) The total size of the document is the same as the size of the <html> element, document.documentElement. You can call getBoundingClientRect() on document.documentElement to get the width and height of the document, or you can use the offsetWidth and offsetHeight properties of document.documentElement. The scroll offsets of the document within its viewport are available as window.scrollX and window.scrollY. These are read-only properties, so you can’t set them to scroll the document: use window.scrollTo() instead. For browser windows, the viewport size is given by the window.innerWidth and window.innerHeight properties. (Web pages optimized for mobile devices often use a <meta name="viewport"> tag in their <head> to set the desired viewport width for the page.) The total size of the document is the same as the size of the <html> element, document.documentElement. You can call getBoundingClientRect() on document.documentElement to get the width and height of the document, or you can use the offsetWidth and offsetHeight properties of document.documentElement. The scroll offsets of the document within its viewport are available as window.scrollX and window.scrollY. These are read-only properties, so you can’t set them to scroll the document: use window.scrollTo() instead.
...@@ -1194,7 +1194,7 @@ clientWidth and clientHeight are like offsetWidth and offsetHeight except that t ...@@ -1194,7 +1194,7 @@ clientWidth and clientHeight are like offsetWidth and offsetHeight except that t
scrollWidth and scrollHeight return the size of an element’s content area plus its padding plus any overflowing content. When the content fits within the content area without overflow, these properties are the same as clientWidth and clientHeight. But when there is overflow, they include the overflowing content and return values larger than clientWidth and clientHeight. scrollLeft and scrollTop give the scroll offset of the element content within the element’s viewport. Unlike all the other properties described here, scrollLeft and scrollTop are writable properties, and you can set them to scroll the content within an element. (In most browsers, Element objects also have scrollTo() and scrollBy() methods like the Window object does, but these are not yet universally supported.) scrollWidth and scrollHeight return the size of an element’s content area plus its padding plus any overflowing content. When the content fits within the content area without overflow, these properties are the same as clientWidth and clientHeight. But when there is overflow, they include the overflowing content and return values larger than clientWidth and clientHeight. scrollLeft and scrollTop give the scroll offset of the element content within the element’s viewport. Unlike all the other properties described here, scrollLeft and scrollTop are writable properties, and you can set them to scroll the content within an element. (In most browsers, Element objects also have scrollTo() and scrollBy() methods like the Window object does, but these are not yet universally supported.)
15.6 Web Components ## 15.6 Web Components
HTML is a language for document markup and defines a rich set of tags for that purpose. Over the last three decades, it has become a language that is used to describe the user interfaces of web applications, but basic HTML tags such as <input> and <button> are inadequate for modern UI designs. Web developers are able to make it work, but only by using CSS and JavaScript to augment the appearance and behavior of basic HTML tags. Consider a typical user interface component, such as the search box shown in Figure 15-3. HTML is a language for document markup and defines a rich set of tags for that purpose. Over the last three decades, it has become a language that is used to describe the user interfaces of web applications, but basic HTML tags such as <input> and <button> are inadequate for modern UI designs. Web developers are able to make it work, but only by using CSS and JavaScript to augment the appearance and behavior of basic HTML tags. Consider a typical user interface component, such as the search box shown in Figure 15-3.
js7e 1503 js7e 1503
...@@ -1205,7 +1205,7 @@ That is a lot of work to do every time you want to display a search box in a web ...@@ -1205,7 +1205,7 @@ That is a lot of work to do every time you want to display a search box in a web
The subsections that follow explain how to use web components defined by other developers in your own web pages, then explain each of the three technologies that web components are based on, and finally tie all three together in an example that implements the search box element pictured in Figure 15-3. The subsections that follow explain how to use web components defined by other developers in your own web pages, then explain each of the three technologies that web components are based on, and finally tie all three together in an example that implements the search box element pictured in Figure 15-3.
15.6.1 Using Web Components ### 15.6.1 Using Web Components
Web components are defined in JavaScript, so in order to use a web component in your HTML file, you need to include the JavaScript file that defines the component. Because web components are a relatively new technology, they are often written as JavaScript modules, so you might include one in your HTML like this: Web components are defined in JavaScript, so in order to use a web component in your HTML file, you need to include the JavaScript file that defines the component. Because web components are a relatively new technology, they are often written as JavaScript modules, so you might include one in your HTML like this:
<script type="module" src="components/search-box.js"> <script type="module" src="components/search-box.js">
...@@ -1244,7 +1244,7 @@ Now that you know how to use web components, the next three sections cover the t ...@@ -1244,7 +1244,7 @@ Now that you know how to use web components, the next three sections cover the t
DOCUMENTFRAGMENT NODES DOCUMENTFRAGMENT NODES
Before we can cover web component APIs, we need to return briefly to the DOM API to explain what a DocumentFragment is. The DOM API organizes a document into a tree of Node objects, where a Node can be a Document, an Element, a Text node, or even a Comment. None of these node types allows you to represent a fragment of a document that consists of a set of sibling nodes without their parent. This is where DocumentFragment comes in: it is another type of Node that serves as a temporary parent when you want to manipulate a group of sibling nodes as a single unit. You can create a DocumentFragment node with document.createDocumentFragment(). Once you have a DocumentFragment, you can use it like an Element and append() content to it. A DocumentFragment is different from an Element because it does not have a parent. But more importantly, when you insert a DocumentFragment node into the document, the DocumentFragment itself is not inserted. Instead, all of its children are inserted. Before we can cover web component APIs, we need to return briefly to the DOM API to explain what a DocumentFragment is. The DOM API organizes a document into a tree of Node objects, where a Node can be a Document, an Element, a Text node, or even a Comment. None of these node types allows you to represent a fragment of a document that consists of a set of sibling nodes without their parent. This is where DocumentFragment comes in: it is another type of Node that serves as a temporary parent when you want to manipulate a group of sibling nodes as a single unit. You can create a DocumentFragment node with document.createDocumentFragment(). Once you have a DocumentFragment, you can use it like an Element and append() content to it. A DocumentFragment is different from an Element because it does not have a parent. But more importantly, when you insert a DocumentFragment node into the document, the DocumentFragment itself is not inserted. Instead, all of its children are inserted.
15.6.2 HTML Templates ### 15.6.2 HTML Templates
The HTML <template> tag is only loosely related to web components, but it does enable a useful optimization for components that appear frequently in web pages. <template> tags and their children are never rendered by a web browser and are only useful on web pages that use JavaScript. The idea behind this tag is that when a web page contains multiple repetitions of the same basic HTML structure (such as rows in a table or the internal implementation of a web component), then we can use a <template> to define that element structure once, then use JavaScript to duplicate the structure as many times as needed. The HTML <template> tag is only loosely related to web components, but it does enable a useful optimization for components that appear frequently in web pages. <template> tags and their children are never rendered by a web browser and are only useful on web pages that use JavaScript. The idea behind this tag is that when a web page contains multiple repetitions of the same basic HTML structure (such as rows in a table or the internal implementation of a web component), then we can use a <template> to define that element structure once, then use JavaScript to duplicate the structure as many times as needed.
In JavaScript, a <template> tag is represented by an HTMLTemplateElement object. This object defines a single content property, and the value of this property is a DocumentFragment of all the child nodes of the <template>. You can clone this DocumentFragment and then insert the cloned copy into your document as needed. The fragment itself will not be inserted, but its children will be. Suppose you’re working with a document that includes a <table> and <template id="row"> tag and that the template defines the structure of rows for that table. You might use the template like this: In JavaScript, a <template> tag is represented by an HTMLTemplateElement object. This object defines a single content property, and the value of this property is a DocumentFragment of all the child nodes of the <template>. You can clone this DocumentFragment and then insert the cloned copy into your document as needed. The fragment itself will not be inserted, but its children will be. Suppose you’re working with a document that includes a <table> and <template id="row"> tag and that the template defines the structure of rows for that table. You might use the template like this:
...@@ -1257,7 +1257,7 @@ let clone = template.content.cloneNode(true); // deep clone ...@@ -1257,7 +1257,7 @@ let clone = template.content.cloneNode(true); // deep clone
tableBody.append(clone); tableBody.append(clone);
Template elements do not have to appear literally in an HTML document in order to be useful. You can create a template in your JavaScript code, create its children with innerHTML, and then make as many clones as needed without the parsing overhead of innerHTML. This is how HTML templates are typically used in web components, and Example 15-3 demonstrates this technique. Template elements do not have to appear literally in an HTML document in order to be useful. You can create a template in your JavaScript code, create its children with innerHTML, and then make as many clones as needed without the parsing overhead of innerHTML. This is how HTML templates are typically used in web components, and Example 15-3 demonstrates this technique.
15.6.3 Custom Elements ### 15.6.3 Custom Elements
The second web browser feature that enables web components is “custom elements”: the ability to associate a JavaScript class with an HTML tag name so that any such tags in the document are automatically turned into instances of the class in the DOM tree. The customElements.define() method takes a web component tag name as its first argument (remember that the tag name must include a hyphen) and a subclass of HTMLElement as its second argument. Any existing elements in the document with that tag name are “upgraded” to newly created instances of the class. And if the browser parses any HTML in the future, it will automatically create an instance of the class for each of the tags it encounters. The second web browser feature that enables web components is “custom elements”: the ability to associate a JavaScript class with an HTML tag name so that any such tags in the document are automatically turned into instances of the class in the DOM tree. The customElements.define() method takes a web component tag name as its first argument (remember that the tag name must include a hyphen) and a subclass of HTMLElement as its second argument. Any existing elements in the document with that tag name are “upgraded” to newly created instances of the class. And if the browser parses any HTML in the future, it will automatically create an instance of the class for each of the tags it encounters.
The class passed to customElements.define() should extend HTMLElement and not a more specific type like HTMLButtonElement.4 Recall from Chapter 9 that when a JavaScript class extends another class, the constructor function must call super() before it uses the this keyword, so if the custom element class has a constructor, it should call super() (with no arguments) before doing anything else. The class passed to customElements.define() should extend HTMLElement and not a more specific type like HTMLButtonElement.4 Recall from Chapter 9 that when a JavaScript class extends another class, the constructor function must call super() before it uses the this keyword, so if the custom element class has a constructor, it should call super() (with no arguments) before doing anything else.
...@@ -1332,7 +1332,7 @@ customElements.define("inline-circle", class InlineCircle extends HTMLElement { ...@@ -1332,7 +1332,7 @@ customElements.define("inline-circle", class InlineCircle extends HTMLElement {
get color() { return this.getAttribute("color"); } get color() { return this.getAttribute("color"); }
set color(color) { this.setAttribute("color", color); } set color(color) { this.setAttribute("color", color); }
}); });
15.6.4 Shadow DOM ### 15.6.4 Shadow DOM
The custom element demonstrated in Example 15-2 is not well encapsulated. When you set its diameter or color attributes, it responds by altering its own style attribute, which is not behavior we would ever expect from a real HTML element. To turn a custom element into a true web component, it should use the powerful encapsulation mechanism known as shadow DOM. The custom element demonstrated in Example 15-2 is not well encapsulated. When you set its diameter or color attributes, it responds by altering its own style attribute, which is not behavior we would ever expect from a real HTML element. To turn a custom element into a true web component, it should use the powerful encapsulation mechanism known as shadow DOM.
Shadow DOM allows a “shadow root” to be attached to a custom element (and also to a <div>, <span>, <body>, <article>, <main>, <nav>, <header>, <footer>, <section>, <p>, <blockquote>, <aside>, or <h1> through <h6> element) known as a “shadow host.” Shadow host elements, like all HTML elements, are already the root of a normal DOM tree of descendant elements and text nodes. A shadow root is the root of another, more private, tree of descendant elements that sprouts from the shadow host and can be thought of as a distinct minidocument. Shadow DOM allows a “shadow root” to be attached to a custom element (and also to a <div>, <span>, <body>, <article>, <main>, <nav>, <header>, <footer>, <section>, <p>, <blockquote>, <aside>, or <h1> through <h6> element) known as a “shadow host.” Shadow host elements, like all HTML elements, are already the root of a normal DOM tree of descendant elements and text nodes. A shadow root is the root of another, more private, tree of descendant elements that sprouts from the shadow host and can be thought of as a distinct minidocument.
...@@ -1366,7 +1366,7 @@ For all of its power, the Shadow DOM doesn’t have much of a JavaScript API. To ...@@ -1366,7 +1366,7 @@ For all of its power, the Shadow DOM doesn’t have much of a JavaScript API. To
If your web component needs to know when the light DOM content of a shadow DOM <slot> has changed, it can register a listener for “slotchanged” events directly on the <slot> element. If your web component needs to know when the light DOM content of a shadow DOM <slot> has changed, it can register a listener for “slotchanged” events directly on the <slot> element.
15.6.5 Example: a <search-box> Web Component ### 15.6.5 Example: a <search-box> Web Component
Figure 15-3 illustrated a <search-box> web component. Example 15-3 demonstrates the three enabling technologies that define web components: it implements the <search-box> component as a custom element that uses a <template> tag for efficiency and a shadow root for encapsulation. Figure 15-3 illustrated a <search-box> web component. Example 15-3 demonstrates the three enabling technologies that define web components: it implements the <search-box> component as a custom element that uses a <template> tag for efficiency and a shadow root for encapsulation.
This example shows how to use the low-level web component APIs directly. In practice, many web components developed today create them using higher-level libraries such as “lit-element.” One of the reasons to use a library is that creating reusable and customizable components is actually quite hard to do well, and there are many details to get right. Example 15-3 demonstrates web components and does some basic keyboard focus handling, but otherwise ignores accessibility and makes no attempt to use proper ARIA attributes to make the component work with screen readers and other assistive technology. This example shows how to use the low-level web component APIs directly. In practice, many web components developed today create them using higher-level libraries such as “lit-element.” One of the reasons to use a library is that creating reusable and customizable components is actually quite hard to do well, and there are many details to get right. Example 15-3 demonstrates web components and does some basic keyboard focus handling, but otherwise ignores accessibility and makes no attempt to use proper ARIA attributes to make the component work with screen readers and other assistive technology.
...@@ -1554,7 +1554,7 @@ slot { ...@@ -1554,7 +1554,7 @@ slot {
// as the implementation of the <search-box> tag. Custom elements are required // as the implementation of the <search-box> tag. Custom elements are required
// to have a tag name that contains a hyphen. // to have a tag name that contains a hyphen.
customElements.define("search-box", SearchBox); customElements.define("search-box", SearchBox);
15.7 SVG: Scalable Vector Graphics ## 15.7 SVG: Scalable Vector Graphics
SVG (scalable vector graphics) is an image format. The word “vector” in its name indicates that it is fundamentally different from raster image formats, such as GIF, JPEG, and PNG, that specify a matrix of pixel values. Instead, an SVG “image” is a precise, resolution-independent (hence “scalable”) description of the steps necessary to draw the desired graphic. SVG images are described by text files using the XML markup language, which is quite similar to HTML. SVG (scalable vector graphics) is an image format. The word “vector” in its name indicates that it is fundamentally different from raster image formats, such as GIF, JPEG, and PNG, that specify a matrix of pixel values. Instead, an SVG “image” is a precise, resolution-independent (hence “scalable”) description of the steps necessary to draw the desired graphic. SVG images are described by text files using the XML markup language, which is quite similar to HTML.
There are three ways you can use SVG in web browsers: There are three ways you can use SVG in web browsers:
...@@ -1567,7 +1567,7 @@ You can use the DOM API to dynamically create SVG elements to generate images on ...@@ -1567,7 +1567,7 @@ You can use the DOM API to dynamically create SVG elements to generate images on
The subsections that follow demonstrate the second and third uses of SVG. Note, however, that SVG has a large and moderately complex grammar. In addition to simple shape-drawing primitives, it includes support for arbitrary curves, text, and animation. SVG graphics can even incorporate JavaScript scripts and CSS stylesheets to add behavior and presentation information. A full description of SVG is well beyond the scope of this book. The goal of this section is just to show you how you can use SVG in your HTML documents and script it with JavaScript. The subsections that follow demonstrate the second and third uses of SVG. Note, however, that SVG has a large and moderately complex grammar. In addition to simple shape-drawing primitives, it includes support for arbitrary curves, text, and animation. SVG graphics can even incorporate JavaScript scripts and CSS stylesheets to add behavior and presentation information. A full description of SVG is well beyond the scope of this book. The goal of this section is just to show you how you can use SVG in your HTML documents and script it with JavaScript.
15.7.1 SVG in HTML ### 15.7.1 SVG in HTML
SVG images can, of course, be displayed using HTML <img> tags. But you can also embed SVG directly in HTML. And if you do this, you can even use CSS stylesheets to specify things like fonts, colors, and line widths. Here, for example, is an HTML file that uses SVG to display an analog clock face: SVG images can, of course, be displayed using HTML <img> tags. But you can also embed SVG directly in HTML. And if you do this, you can even use CSS stylesheets to specify things like fonts, colors, and line widths. Here, for example, is an HTML file that uses SVG to display an analog clock face:
<html> <html>
...@@ -1622,7 +1622,7 @@ SVG images can, of course, be displayed using HTML <img> tags. But you can also ...@@ -1622,7 +1622,7 @@ SVG images can, of course, be displayed using HTML <img> tags. But you can also
</html> </html>
You’ll notice that the descendants of the <svg> tag are not normal HTML tags. <circle>, <line>, and <text> tags have obvious purposes, though, and it should be clear how this SVG graphic works. There are many other SVG tags, however, and you’ll need to consult an SVG reference to learn more. You may also notice that the stylesheet is odd. Styles like fill, stroke-width, and text-anchor are not normal CSS style properties. In this case, CSS is essentially being used to set attributes of SVG tags that appear in the document. Note also that the CSS font shorthand property does not work for SVG tags, and you must explicitly set font-family, font-size, and font-weight as separate style properties. You’ll notice that the descendants of the <svg> tag are not normal HTML tags. <circle>, <line>, and <text> tags have obvious purposes, though, and it should be clear how this SVG graphic works. There are many other SVG tags, however, and you’ll need to consult an SVG reference to learn more. You may also notice that the stylesheet is odd. Styles like fill, stroke-width, and text-anchor are not normal CSS style properties. In this case, CSS is essentially being used to set attributes of SVG tags that appear in the document. Note also that the CSS font shorthand property does not work for SVG tags, and you must explicitly set font-family, font-size, and font-weight as separate style properties.
15.7.2 Scripting SVG ### 15.7.2 Scripting SVG
One reason to embed SVG directly into your HTML files (instead of just using static <img> tags) is that if you do this, then you can use the DOM API to manipulate the SVG image. Suppose you use SVG to display icons in your web application. You could embed SVG within a <template> tag (§15.6.2) and then clone the template content whenever you need to insert a copy of that icon into your UI. And if you want the icon to respond to user activity—by changing color when the user hovers the pointer over it, for example—you can often achieve this with CSS. One reason to embed SVG directly into your HTML files (instead of just using static <img> tags) is that if you do this, then you can use the DOM API to manipulate the SVG image. Suppose you use SVG to display icons in your web application. You could embed SVG within a <template> tag (§15.6.2) and then clone the template content whenever you need to insert a copy of that icon into your UI. And if you want the icon to respond to user activity—by changing color when the user hovers the pointer over it, for example—you can often achieve this with CSS.
It is also possible to dynamically manipulate SVG graphics that are directly embedded in HTML. The clock face example in the previous section displays a static clock with hour and minute hands facing straight up displaying the time noon or midnight. But you may have noticed that the HTML file includes a <script> tag. That script runs a function periodically to check the time and transform the hour and minute hands by rotating them the appropriate number of degrees so that the clock actually displays the current time, as shown in Figure 15-5. It is also possible to dynamically manipulate SVG graphics that are directly embedded in HTML. The clock face example in the previous section displays a static clock with hour and minute hands facing straight up displaying the time noon or midnight. But you may have noticed that the HTML file includes a <script> tag. That script runs a function periodically to check the time and transform the hour and minute hands by rotating them the appropriate number of degrees so that the clock actually displays the current time, as shown in Figure 15-5.
...@@ -1650,7 +1650,7 @@ The code to manipulate the clock is straightforward. It determines the proper an ...@@ -1650,7 +1650,7 @@ The code to manipulate the clock is straightforward. It determines the proper an
// Run this function again in 10 seconds // Run this function again in 10 seconds
setTimeout(updateClock, 10000); setTimeout(updateClock, 10000);
}()); // Note immediate invocation of the function here. }()); // Note immediate invocation of the function here.
15.7.3 Creating SVG Images with JavaScript ### 15.7.3 Creating SVG Images with JavaScript
In addition to simply scripting SVG images embedded in your HTML documents, you can also build SVG images from scratch, which can be useful to create visualizations of dynamically loaded data, for example. Example 15-4 demonstrates how you can use JavaScript to create SVG pie charts, like the one shown in Figure 15-6. In addition to simply scripting SVG images embedded in your HTML documents, you can also build SVG images from scratch, which can be useful to create visualizations of dynamically loaded data, for example. Example 15-4 demonstrates how you can use JavaScript to create SVG pie charts, like the one shown in Figure 15-6.
Even though SVG tags can be included within HTML documents, they are technically XML tags, not HTML tags, and if you want to create SVG elements with the JavaScript DOM API, you can’t use the normal createElement() function that was introduced in §15.3.5. Instead you must use createElementNS(), which takes an XML namespace string as its first argument. For SVG, that namespace is the literal string “http://www.w3.org/2000/svg.” Even though SVG tags can be included within HTML documents, they are technically XML tags, not HTML tags, and if you want to create SVG elements with the JavaScript DOM API, you can’t use the normal createElement() function that was introduced in §15.3.5. Instead you must use createElementNS(), which takes an XML namespace string as its first argument. For SVG, that namespace is the literal string “http://www.w3.org/2000/svg.”
...@@ -1782,7 +1782,7 @@ document.querySelector("#chart").append(pieChart({ ...@@ -1782,7 +1782,7 @@ document.querySelector("#chart").append(pieChart({
"Go": 7.2, "Go": 7.2,
} }
})); }));
15.8 Graphics in a <canvas> ## 15.8 Graphics in a <canvas>
The <canvas> element has no appearance of its own but creates a drawing surface within the document and exposes a powerful drawing API to client-side JavaScript. The main difference between the <canvas> API and SVG is that with the canvas you create drawings by calling methods, and with SVG you create drawings by building a tree of XML elements. These two approaches are equivalently powerful: either one can be simulated with the other. On the surface, they are quite different, however, and each has its strengths and weaknesses. An SVG drawing, for example, is easily edited by removing elements from its description. To remove an element from the same graphic in a <canvas>, it is often necessary to erase the drawing and redraw it from scratch. Since the Canvas drawing API is JavaScript-based and relatively compact (unlike the SVG grammar), it is documented in more detail in this book. The <canvas> element has no appearance of its own but creates a drawing surface within the document and exposes a powerful drawing API to client-side JavaScript. The main difference between the <canvas> API and SVG is that with the canvas you create drawings by calling methods, and with SVG you create drawings by building a tree of XML elements. These two approaches are equivalently powerful: either one can be simulated with the other. On the surface, they are quite different, however, and each has its strengths and weaknesses. An SVG drawing, for example, is easily edited by removing elements from its description. To remove an element from the same graphic in a <canvas>, it is often necessary to erase the drawing and redraw it from scratch. Since the Canvas drawing API is JavaScript-based and relatively compact (unlike the SVG grammar), it is documented in more detail in this book.
3D GRAPHICS IN A CANVAS 3D GRAPHICS IN A CANVAS
...@@ -1813,7 +1813,7 @@ The subsections that follow demonstrate the methods and properties of the 2D Can ...@@ -1813,7 +1813,7 @@ The subsections that follow demonstrate the methods and properties of the 2D Can
let canvas = document.querySelector("#my_canvas_id"); let canvas = document.querySelector("#my_canvas_id");
let c = canvas.getContext('2d'); let c = canvas.getContext('2d');
15.8.1 Paths and Polygons ### 15.8.1 Paths and Polygons
To draw lines on a canvas and to fill the areas enclosed by those lines, you begin by defining a path. A path is a sequence of one or more subpaths. A subpath is a sequence of two or more points connected by line segments (or, as we’ll see later, by curve segments). Begin a new path with the beginPath() method. Begin a new subpath with the moveTo() method. Once you have established the starting point of a subpath with moveTo(), you can connect that point to a new point with a straight line by calling lineTo(). The following code defines a path that includes two line segments: To draw lines on a canvas and to fill the areas enclosed by those lines, you begin by defining a path. A path is a sequence of one or more subpaths. A subpath is a sequence of two or more points connected by line segments (or, as we’ll see later, by curve segments). Begin a new path with the beginPath() method. Begin a new subpath with the moveTo() method. Once you have established the starting point of a subpath with moveTo(), you can connect that point to a new point with a straight line by calling lineTo(). The following code defines a path that includes two line segments:
c.beginPath(); // Start a new path c.beginPath(); // Start a new path
...@@ -1882,7 +1882,7 @@ c.fill(); // Fill the shapes ...@@ -1882,7 +1882,7 @@ c.fill(); // Fill the shapes
c.stroke(); // And stroke their outlines c.stroke(); // And stroke their outlines
Notice that this example draws a hexagon with a square inside it. The square and the hexagon are separate subpaths, but they overlap. When this happens (or when a single subpath intersects itself), the canvas needs to be able to determine which regions are inside the path and which are outside. The canvas uses a test known as the “nonzero winding rule” to achieve this. In this case, the interior of the square is not filled because the square and the hexagon were drawn in the opposite directions: the vertices of the hexagon were connected with line segments moving clockwise around the circle. The vertices of the square were connected counterclockwise. Had the square been drawn clockwise as well, the call to fill() would have filled the interior of the square as well. Notice that this example draws a hexagon with a square inside it. The square and the hexagon are separate subpaths, but they overlap. When this happens (or when a single subpath intersects itself), the canvas needs to be able to determine which regions are inside the path and which are outside. The canvas uses a test known as the “nonzero winding rule” to achieve this. In this case, the interior of the square is not filled because the square and the hexagon were drawn in the opposite directions: the vertices of the hexagon were connected with line segments moving clockwise around the circle. The vertices of the square were connected counterclockwise. Had the square been drawn clockwise as well, the call to fill() would have filled the interior of the square as well.
15.8.2 Canvas Dimensions and Coordinates ### 15.8.2 Canvas Dimensions and Coordinates
The width and height attributes of the <canvas> element and the corresponding width and height properties of the Canvas object specify the dimensions of the canvas. The default canvas coordinate system places the origin (0,0) at the upper-left corner of the canvas. The x coordinates increase to the right and the y coordinates increase as you go down the screen. Points on the canvas can be specified using floating-point values. The width and height attributes of the <canvas> element and the corresponding width and height properties of the Canvas object specify the dimensions of the canvas. The default canvas coordinate system places the origin (0,0) at the upper-left corner of the canvas. The x coordinates increase to the right and the y coordinates increase as you go down the screen. Points on the canvas can be specified using floating-point values.
The dimensions of a canvas cannot be altered without completely resetting the canvas. Setting either the width or height properties of a Canvas (even setting them to their current value) clears the canvas, erases the current path, and resets all graphics attributes (including current transformation and clipping region) to their original state. The dimensions of a canvas cannot be altered without completely resetting the canvas. Setting either the width or height properties of a Canvas (even setting them to their current value) clears the canvas, erases the current path, and resets all graphics attributes (including current transformation and clipping region) to their original state.
...@@ -1893,7 +1893,7 @@ The width and height attributes also specify the default size (in CSS pixels) at ...@@ -1893,7 +1893,7 @@ The width and height attributes also specify the default size (in CSS pixels) at
For optimum image quality, you should not use the width and height attributes to set the on-screen size of the canvas. Instead, set the desired on-screen size CSS pixel size of the canvas with CSS width and height style attributes. Then, before you begin drawing in your JavaScript code, set the width and height properties of the canvas object to the number of CSS pixels times window.devicePixelRatio. Continuing with the preceding example, this technique would result in the canvas being displayed at 100 × 100 CSS pixels but allocating memory for 200 × 200 pixels. (Even with this technique, the user can zoom in on the canvas and may see fuzzy or pixelated graphics if they do. This is in contrast to SVG graphics, which remain crisp no matter the on-screen size or zoom level.) For optimum image quality, you should not use the width and height attributes to set the on-screen size of the canvas. Instead, set the desired on-screen size CSS pixel size of the canvas with CSS width and height style attributes. Then, before you begin drawing in your JavaScript code, set the width and height properties of the canvas object to the number of CSS pixels times window.devicePixelRatio. Continuing with the preceding example, this technique would result in the canvas being displayed at 100 × 100 CSS pixels but allocating memory for 200 × 200 pixels. (Even with this technique, the user can zoom in on the canvas and may see fuzzy or pixelated graphics if they do. This is in contrast to SVG graphics, which remain crisp no matter the on-screen size or zoom level.)
15.8.3 Graphics Attributes ### 15.8.3 Graphics Attributes
Example 15-5 set the properties fillStyle, strokeStyle, and lineWidth on the context object of the canvas. These properties are graphics attributes that specify the color to be used by fill() and by stroke(), and the width of the lines to be drawn by stroke(). Notice that these parameters are not passed to the fill() and stroke() methods, but are instead part of the general graphics state of the canvas. If you define a method that draws a shape and do not set these properties yourself, the caller of your method can define the color of the shape by setting the strokeStyle and fillStyle properties before calling your method. This separation of graphics state from drawing commands is fundamental to the Canvas API and is akin to the separation of presentation from content achieved by applying CSS stylesheets to HTML documents. Example 15-5 set the properties fillStyle, strokeStyle, and lineWidth on the context object of the canvas. These properties are graphics attributes that specify the color to be used by fill() and by stroke(), and the width of the lines to be drawn by stroke(). Notice that these parameters are not passed to the fill() and stroke() methods, but are instead part of the general graphics state of the canvas. If you define a method that draws a shape and do not set these properties yourself, the caller of your method can define the color of the shape by setting the strokeStyle and fillStyle properties before calling your method. This separation of graphics state from drawing commands is fundamental to the Canvas API and is akin to the separation of presentation from content achieved by applying CSS stylesheets to HTML documents.
There are a number of properties (and also some methods) on the context object that affect the graphics state of the canvas. They are detailed below. There are a number of properties (and also some methods) on the context object that affect the graphics state of the canvas. They are detailed below.
...@@ -1964,7 +1964,7 @@ Since the Canvas API defines graphics attributes on the context object, you migh ...@@ -1964,7 +1964,7 @@ Since the Canvas API defines graphics attributes on the context object, you migh
Although the Canvas API only allows you to define a single set of graphics attributes at a time, it does allow you to save the current graphics state so that you can alter it and then easily restore it later. The save() method pushes the current graphics state onto a stack of saved states. The restore() method pops the stack and restores the most recently saved state. All of the properties that have been described in this section are part of the saved state, as are the current transformation and clipping region (both of which are explained later). Importantly, the currently defined path and the current point are not part of the graphics state and cannot be saved and restored. Although the Canvas API only allows you to define a single set of graphics attributes at a time, it does allow you to save the current graphics state so that you can alter it and then easily restore it later. The save() method pushes the current graphics state onto a stack of saved states. The restore() method pops the stack and restores the most recently saved state. All of the properties that have been described in this section are part of the saved state, as are the current transformation and clipping region (both of which are explained later). Importantly, the currently defined path and the current point are not part of the graphics state and cannot be saved and restored.
15.8.4 Canvas Drawing Operations ### 15.8.4 Canvas Drawing Operations
We’ve already seen some basic canvas methods—beginPath(), moveTo(), lineTo(), closePath(), fill(), and stroke()—for defining, filling, and drawing lines and polygons. But the Canvas API includes other drawing methods as well. We’ve already seen some basic canvas methods—beginPath(), moveTo(), lineTo(), closePath(), fill(), and stroke()—for defining, filling, and drawing lines and polygons. But the Canvas API includes other drawing methods as well.
RECTANGLES RECTANGLES
...@@ -2085,7 +2085,7 @@ In addition to drawing images into a canvas, we can also extract the content of ...@@ -2085,7 +2085,7 @@ In addition to drawing images into a canvas, we can also extract the content of
let img = document.createElement("img"); // Create an <img> element let img = document.createElement("img"); // Create an <img> element
img.src = canvas.toDataURL(); // Set its src attribute img.src = canvas.toDataURL(); // Set its src attribute
document.body.appendChild(img); // Append it to the document document.body.appendChild(img); // Append it to the document
15.8.5 Coordinate System Transforms ### 15.8.5 Coordinate System Transforms
As we’ve noted, the default coordinate system of a canvas places the origin in the upper-left corner, has x coordinates increasing to the right, and has y coordinates increasing downward. In this default system, the coordinates of a point map directly to a CSS pixel (which then maps directly to one or more device pixels). Certain canvas operations and attributes (such as extracting raw pixel values and setting shadow offsets) always use this default coordinate system. In addition to the default coordinate system, however, every canvas has a “current transformation matrix” as part of its graphics state. This matrix defines the current coordinate system of the canvas. In most canvas operations, when you specify the coordinates of a point, it is taken to be a point in the current coordinate system, not in the default coordinate system. The current transformation matrix is used to convert the coordinates you specified to the equivalent coordinates in the default coordinate system. As we’ve noted, the default coordinate system of a canvas places the origin in the upper-left corner, has x coordinates increasing to the right, and has y coordinates increasing downward. In this default system, the coordinates of a point map directly to a CSS pixel (which then maps directly to one or more device pixels). Certain canvas operations and attributes (such as extracting raw pixel values and setting shadow offsets) always use this default coordinate system. In addition to the default coordinate system, however, every canvas has a “current transformation matrix” as part of its graphics state. This matrix defines the current coordinate system of the canvas. In most canvas operations, when you specify the coordinates of a point, it is taken to be a point in the current coordinate system, not in the default coordinate system. The current transformation matrix is used to convert the coordinates you specified to the equivalent coordinates in the default coordinate system.
The setTransform() method allows you to set a canvas’s transformation matrix directly, but coordinate system transformations are usually easier to specify as a sequence of translations, rotations, and scaling operations. Figure 15-11 illustrates these operations and their effect on the canvas coordinate system. The program that produced the figure drew the same set of axes seven times in a row. The only thing that changed each time was the current transform. Notice that the transforms affect the text as well as the lines that are drawn. The setTransform() method allows you to set a canvas’s transformation matrix directly, but coordinate system transformations are usually easier to specify as a sequence of translations, rotations, and scaling operations. Figure 15-11 illustrates these operations and their effect on the canvas coordinate system. The program that produced the figure drew the same set of axes seven times in a row. The only thing that changed each time was the current transform. Notice that the transforms affect the text as well as the lines that are drawn.
...@@ -2203,7 +2203,7 @@ snowflake(c, 2, 325, 125, 125); // etc. ...@@ -2203,7 +2203,7 @@ snowflake(c, 2, 325, 125, 125); // etc.
snowflake(c, 3, 475, 125, 125); snowflake(c, 3, 475, 125, 125);
snowflake(c, 4, 625, 125, 125); // A level-4 snowflake looks like a snowflake! snowflake(c, 4, 625, 125, 125); // A level-4 snowflake looks like a snowflake!
c.stroke(); // Stroke this very complicated path c.stroke(); // Stroke this very complicated path
15.8.6 Clipping ### 15.8.6 Clipping
After defining a path, you usually call stroke() or fill() (or both). You can also call the clip() method to define a clipping region. Once a clipping region is defined, nothing will be drawn outside of it. Figure 15-13 shows a complex drawing produced using clipping regions. The vertical stripe running down the middle and the text along the bottom of the figure were stroked with no clipping region and then filled after the triangular clipping region was defined. After defining a path, you usually call stroke() or fill() (or both). You can also call the clip() method to define a clipping region. Once a clipping region is defined, nothing will be drawn outside of it. Figure 15-13 shows a complex drawing produced using clipping regions. The vertical stripe running down the middle and the text along the bottom of the figure were stroked with no clipping region and then filled after the triangular clipping region was defined.
js7e 1512 js7e 1512
...@@ -2237,7 +2237,7 @@ c.fillStyle = "#888"; // Darker gray ...@@ -2237,7 +2237,7 @@ c.fillStyle = "#888"; // Darker gray
c.fillText("<canvas>", 15, 330); // Fill the text c.fillText("<canvas>", 15, 330); // Fill the text
It is important to note that when you call clip(), the current path is itself clipped to the current clipping region, then that clipped path becomes the new clipping region. This means that the clip() method can shrink the clipping region but can never enlarge it. There is no method to reset the clipping region, so before calling clip(), you should typically call save() so that you can later restore() the unclipped region. It is important to note that when you call clip(), the current path is itself clipped to the current clipping region, then that clipped path becomes the new clipping region. This means that the clip() method can shrink the clipping region but can never enlarge it. There is no method to reset the clipping region, so before calling clip(), you should typically call save() so that you can later restore() the unclipped region.
15.8.7 Pixel Manipulation ### 15.8.7 Pixel Manipulation
The getImageData() method returns an ImageData object that represents the raw pixels (as R, G, B, and A components) from a rectangular region of your canvas. You can create empty ImageData objects with createImageData(). The pixels in an ImageData object are writable, so you can set them any way you want, then copy those pixels back onto the canvas with putImageData(). The getImageData() method returns an ImageData object that represents the raw pixels (as R, G, B, and A components) from a rectangular region of your canvas. You can create empty ImageData objects with createImageData(). The pixels in an ImageData object are writable, so you can set them any way you want, then copy those pixels back onto the canvas with putImageData().
These pixel manipulation methods provide very low-level access to the canvas. The rectangle you pass to getImageData() is in the default coordinate system: its dimensions are measured in CSS pixels, and it is not affected by the current transformation. When you call putImageData(), the position you specify is also measured in the default coordinate system. Furthermore, putImageData() ignores all graphics attributes. It does not perform any compositing, it does not multiply pixels by globalAlpha, and it does not draw shadows. These pixel manipulation methods provide very low-level access to the canvas. The rectangle you pass to getImageData() is in the default coordinate system: its dimensions are measured in CSS pixels, and it is not affected by the current transformation. When you call putImageData(), the position you specify is also measured in the default coordinate system. Furthermore, putImageData() ignores all graphics attributes. It does not perform any compositing, it does not multiply pixels by globalAlpha, and it does not draw shadows.
...@@ -2287,12 +2287,12 @@ function smear(c, n, x, y, w, h) { ...@@ -2287,12 +2287,12 @@ function smear(c, n, x, y, w, h) {
// Now copy the smeared image data back to the same position on the canvas // Now copy the smeared image data back to the same position on the canvas
c.putImageData(pixels, x, y); c.putImageData(pixels, x, y);
} }
15.9 Audio APIs ## 15.9 Audio APIs
The HTML <audio> and <video> tags allow you to easily include sound and videos in your web pages. These are complex elements with significant APIs and nontrivial user interfaces. You can control media playback with the play() and pause() methods. You can set the volume and playbackRate properties to control the audio volume and speed of playback. And you can skip to a particular time within the media by setting the currentTime property. The HTML <audio> and <video> tags allow you to easily include sound and videos in your web pages. These are complex elements with significant APIs and nontrivial user interfaces. You can control media playback with the play() and pause() methods. You can set the volume and playbackRate properties to control the audio volume and speed of playback. And you can skip to a particular time within the media by setting the currentTime property.
We will not cover <audio> and <video> tags in any further detail here, however. The following subsections demonstrate two ways to add scripted sound effects to your web pages. We will not cover <audio> and <video> tags in any further detail here, however. The following subsections demonstrate two ways to add scripted sound effects to your web pages.
15.9.1 The Audio() Constructor ### 15.9.1 The Audio() Constructor
You don’t have to include an <audio> tag in your HTML document in order to include sound effects in your web pages. You can dynamically create <audio> elements with the normal DOM document.createElement() method, or, as a shortcut, you can simply use the Audio() constructor. You do not have to add the created element to your document in order to play it. You can simply call its play() method: You don’t have to include an <audio> tag in your HTML document in order to include sound effects in your web pages. You can dynamically create <audio> elements with the normal DOM document.createElement() method, or, as a shortcut, you can simply use the Audio() constructor. You do not have to add the created element to your document in order to play it. You can simply call its play() method:
// Load the sound effect in advance so it is ready for use // Load the sound effect in advance so it is ready for use
...@@ -2304,7 +2304,7 @@ document.addEventListener("click", () => { ...@@ -2304,7 +2304,7 @@ document.addEventListener("click", () => {
}); });
Note the use of cloneNode() here. If the user clicks the mouse rapidly, we want to be able to have multiple overlapping copies of the sound effect playing at the same time. To do that, we need multiple Audio elements. Because the Audio elements are not added to the document, they will be garbage collected when they are done playing. Note the use of cloneNode() here. If the user clicks the mouse rapidly, we want to be able to have multiple overlapping copies of the sound effect playing at the same time. To do that, we need multiple Audio elements. Because the Audio elements are not added to the document, they will be garbage collected when they are done playing.
15.9.2 The WebAudio API ### 15.9.2 The WebAudio API
In addition to playback of recorded sounds with Audio elements, web browsers also allow the generation and playback of synthesized sounds with the WebAudio API. Using the WebAudio API is like hooking up an old-style electronic synthesizer with patch cords. With WebAudio, you create a set of AudioNode objects, which represents sources, transformations, or destinations of waveforms, and then connect these nodes together into a network to produce sounds. The API is not particularly complex, but a full explanation requires an understanding of electronic music and signal processing concepts that are beyond the scope of this book. In addition to playback of recorded sounds with Audio elements, web browsers also allow the generation and playback of synthesized sounds with the WebAudio API. Using the WebAudio API is like hooking up an old-style electronic synthesizer with patch cords. With WebAudio, you create a set of AudioNode objects, which represents sources, transformations, or destinations of waveforms, and then connect these nodes together into a network to produce sounds. The API is not particularly complex, but a full explanation requires an understanding of electronic music and signal processing concepts that are beyond the scope of this book.
The following code below uses the WebAudio API to synthesize a short chord that fades out over about a second. This example demonstrates the basics of the WebAudio API. If this is interesting to you, you can find much more about this API online: The following code below uses the WebAudio API to synthesize a short chord that fades out over about a second. This example demonstrates the basics of the WebAudio API. If this is interesting to you, you can find much more about this API online:
...@@ -2352,7 +2352,7 @@ oscillators.forEach(o => { ...@@ -2352,7 +2352,7 @@ oscillators.forEach(o => {
oscillators[0].addEventListener("ended", () => { oscillators[0].addEventListener("ended", () => {
// This event handler is invoked when the note stops playing // This event handler is invoked when the note stops playing
}); });
15.10 Location, Navigation, and History ## 15.10 Location, Navigation, and History
The location property of both the Window and Document objects refers to the Location object, which represents the current URL of the document displayed in the window, and which also provides an API for loading new documents into the window. The location property of both the Window and Document objects refers to the Location object, which represents the current URL of the document displayed in the window, and which also provides an API for loading new documents into the window.
The Location object is very much like a URL object (§11.9), and you can use properties like protocol, hostname, port, and path to access the various parts of the URL of the current document. The href property returns the entire URL as a string, as does the toString() method. The Location object is very much like a URL object (§11.9), and you can use properties like protocol, hostname, port, and path to access the various parts of the URL of the current document. The href property returns the entire URL as a string, as does the toString() method.
...@@ -2366,7 +2366,7 @@ let query = url.searchParams.get("q"); ...@@ -2366,7 +2366,7 @@ let query = url.searchParams.get("q");
let numResults = parseInt(url.searchParams.get("n") || "10"); let numResults = parseInt(url.searchParams.get("n") || "10");
In addition to the Location object that you can refer to as window.location or document.location, and the URL() constructor that we used earlier, browsers also define a document.URL property. Surprisingly, the value of this property is not a URL object, but just a string. The string holds the URL of the current document. In addition to the Location object that you can refer to as window.location or document.location, and the URL() constructor that we used earlier, browsers also define a document.URL property. Surprisingly, the value of this property is not a URL object, but just a string. The string holds the URL of the current document.
15.10.1 Loading New Documents ### 15.10.1 Loading New Documents
If you assign a string to window.location or to document.location, that string is interpreted as a URL and the browser loads it, replacing the current document with a new one: If you assign a string to window.location or to document.location, that string is interpreted as a URL and the browser loads it, replacing the current document with a new one:
window.location = "http://www.oreilly.com"; // Go buy some books! window.location = "http://www.oreilly.com"; // Go buy some books!
...@@ -2394,7 +2394,7 @@ Notice that the URL passed to replace() is a relative one. Relative URLs are int ...@@ -2394,7 +2394,7 @@ Notice that the URL passed to replace() is a relative one. Relative URLs are int
In addition to the assign() and replace() methods, the Location object also defines reload(), which simply makes the browser reload the document. In addition to the assign() and replace() methods, the Location object also defines reload(), which simply makes the browser reload the document.
15.10.2 Browsing History ### 15.10.2 Browsing History
The history property of the Window object refers to the History object for the window. The History object models the browsing history of a window as a list of documents and document states. The length property of the History object specifies the number of elements in the browsing history list, but for security reasons, scripts are not allowed to access the stored URLs. (If they could, any scripts could snoop through your browsing history.) The history property of the Window object refers to the History object for the window. The History object models the browsing history of a window as a list of documents and document states. The length property of the History object specifies the number of elements in the browsing history list, but for security reasons, scripts are not allowed to access the stored URLs. (If they could, any scripts could snoop through your browsing history.)
The History object has back() and forward() methods that behave like the browser’s Back and Forward buttons do: they make the browser go backward or forward one step in its browsing history. A third method, go(), takes an integer argument and can skip any number of pages forward (for positive arguments) or backward (for negative arguments) in the history list: The History object has back() and forward() methods that behave like the browser’s Back and Forward buttons do: they make the browser go backward or forward one step in its browsing history. A third method, go(), takes an integer argument and can skip any number of pages forward (for positive arguments) or backward (for negative arguments) in the history list:
...@@ -2405,7 +2405,7 @@ If a window contains child windows (such as <iframe> elements), the browsing his ...@@ -2405,7 +2405,7 @@ If a window contains child windows (such as <iframe> elements), the browsing his
The History object described here dates back to the early days of the web when documents were passive and all computation was performed on the server. Today, web applications often generate or load content dynamically and display new application states without actually loading new documents. Applications like these must perform their own history management if they want the user to be able to use the Back and Forward buttons (or the equivalent gestures) to navigate from one application state to another in an intuitive way. There are two ways to accomplish this, described in the next two sections. The History object described here dates back to the early days of the web when documents were passive and all computation was performed on the server. Today, web applications often generate or load content dynamically and display new application states without actually loading new documents. Applications like these must perform their own history management if they want the user to be able to use the Back and Forward buttons (or the equivalent gestures) to navigate from one application state to another in an intuitive way. There are two ways to accomplish this, described in the next two sections.
15.10.3 History Management with hashchange Events ### 15.10.3 History Management with hashchange Events
One history management technique involves location.hash and the “hashchange” event. Here are the key facts you need to know to understand this technique: One history management technique involves location.hash and the “hashchange” event. Here are the key facts you need to know to understand this technique:
The location.hash property sets the fragment identifier of the URL and is traditionally used to specify the ID of a document section to scroll to. But location.hash does not have to be an element ID: you can set it to any string. As long as no element happens to have that string as its ID, the browser won’t scroll when you set the hash property like this. The location.hash property sets the fragment identifier of the URL and is traditionally used to specify the ID of a document section to scroll to. But location.hash does not have to be an element ID: you can set it to any string. As long as no element happens to have that string as its ID, the browser won’t scroll when you set the hash property like this.
...@@ -2420,7 +2420,7 @@ Once you have written those functions, the rest is easy. Define a window.onhashc ...@@ -2420,7 +2420,7 @@ Once you have written those functions, the rest is easy. Define a window.onhashc
When the user interacts with your application (such as by clicking a link) in a way that would cause the application to enter a new state, don’t render the new state directly. Instead, encode the desired new state as a string and set location.hash to that string. This will trigger a “hashchange” event, and your handler for that event will display the new state. Using this roundabout technique ensures that the new state is inserted into the browsing history so that the Back and Forward buttons continue to work. When the user interacts with your application (such as by clicking a link) in a way that would cause the application to enter a new state, don’t render the new state directly. Instead, encode the desired new state as a string and set location.hash to that string. This will trigger a “hashchange” event, and your handler for that event will display the new state. Using this roundabout technique ensures that the new state is inserted into the browsing history so that the Back and Forward buttons continue to work.
15.10.4 History Management with pushState() ### 15.10.4 History Management with pushState()
The second technique for managing history is somewhat more complex but is less of a hack than the “hashchange” event. This more robust history-management technique is based on the history.pushState() method and the “popstate” event. When a web app enters a new state, it calls history.pushState() to add an object representing the state to the browser’s history. If the user then clicks the Back button, the browser fires a “popstate” event with a copy of that saved state object, and the app uses that object to re-create its previous state. In addition to the saved state object, applications can also save a URL with each state, which is important if you want users to be able to bookmark and share links to the internal states of the app. The second technique for managing history is somewhat more complex but is less of a hack than the “hashchange” event. This more robust history-management technique is based on the history.pushState() method and the “popstate” event. When a web app enters a new state, it calls history.pushState() to add an object representing the state to the browser’s history. If the user then clicks the Back button, the browser fires a “popstate” event with a copy of that saved state object, and the app uses that object to re-create its previous state. In addition to the saved state object, applications can also save a URL with each state, which is important if you want users to be able to bookmark and share links to the internal states of the app.
The first argument to pushState() is an object that contains all the state information necessary to restore the current state of the document. This object is saved using HTML’s structured clone algorithm, which is more versatile than JSON.stringify() and can support Map, Set, and Date objects as well as typed arrays and ArrayBuffers. The first argument to pushState() is an object that contains all the state information necessary to restore the current state of the document. This object is saved using HTML’s structured clone algorithm, which is more versatile than JSON.stringify() and can support Map, Set, and Date objects as well as typed arrays and ArrayBuffers.
...@@ -2627,7 +2627,7 @@ window.onpopstate = (event) => { ...@@ -2627,7 +2627,7 @@ window.onpopstate = (event) => {
}; };
</script> </script>
</body></html> </body></html>
15.11 Networking ## 15.11 Networking
Every time you load a web page, the browser makes network requests—using the HTTP and HTTPS protocols—for an HTML file as well as the images, fonts, scripts, and stylesheets that the file depends on. But in addition to being able to make network requests in response to user actions, web browsers also expose JavaScript APIs for networking as well. Every time you load a web page, the browser makes network requests—using the HTTP and HTTPS protocols—for an HTML file as well as the images, fonts, scripts, and stylesheets that the file depends on. But in addition to being able to make network requests in response to user actions, web browsers also expose JavaScript APIs for networking as well.
This section covers three network APIs: This section covers three network APIs:
...@@ -2638,7 +2638,7 @@ The Server-Sent Events (or SSE) API is a convenient, event-based interface to HT ...@@ -2638,7 +2638,7 @@ The Server-Sent Events (or SSE) API is a convenient, event-based interface to HT
WebSockets is a networking protocol that is not HTTP but is designed to interoperate with HTTP. It defines an asynchronous message-passing API where clients and servers can send and receive messages from each other in a way that is similar to TCP network sockets. WebSockets is a networking protocol that is not HTTP but is designed to interoperate with HTTP. It defines an asynchronous message-passing API where clients and servers can send and receive messages from each other in a way that is similar to TCP network sockets.
15.11.1 fetch() ### 15.11.1 fetch()
For basic HTTP requests, using fetch() is a three-step process: For basic HTTP requests, using fetch() is a three-step process:
Call fetch(), passing the URL whose content you want to retrieve. Call fetch(), passing the URL whose content you want to retrieve.
...@@ -2922,7 +2922,7 @@ This value means that you want to manually handle redirect responses, and the Pr ...@@ -2922,7 +2922,7 @@ This value means that you want to manually handle redirect responses, and the Pr
referrer referrer
You can set this property to a string that contains a relative URL to specify the value of the HTTP “Referer” header (which is historically misspelled with three Rs instead of four). If you set this property to the empty string, then the “Referer” header will be omitted from the request. You can set this property to a string that contains a relative URL to specify the value of the HTTP “Referer” header (which is historically misspelled with three Rs instead of four). If you set this property to the empty string, then the “Referer” header will be omitted from the request.
15.11.2 Server-Sent Events ### 15.11.2 Server-Sent Events
A fundamental feature of the HTTP protocol upon which the web is built is that clients initiate requests and servers respond to those requests. Some web apps find it useful, however, to have their server send them notifications when events occur. This does not come naturally to HTTP, but the technique that has been devised is for the client to make a request to the server, and then neither the client nor the server close the connection. When the server has something to tell the client about, it writes data to the connection but keeps it open. The effect is as if the client makes a network request and the server responds in a slow and bursty way with significant pauses between bursts of activity. Network connections like this don’t usually stay open forever, but if the client detects that the connection has closed, it can simply make another request to reopen the connection. A fundamental feature of the HTTP protocol upon which the web is built is that clients initiate requests and servers respond to those requests. Some web apps find it useful, however, to have their server send them notifications when events occur. This does not come naturally to HTTP, but the technique that has been devised is for the client to make a request to the server, and then neither the client nor the server close the connection. When the server has something to tell the client about, it writes data to the connection but keeps it open. The effect is as if the client makes a network request and the server responds in a slow and bursty way with significant pauses between bursts of activity. Network connections like this don’t usually stay open forever, but if the client detects that the connection has closed, it can simply make another request to reopen the connection.
This technique for allowing servers to send messages to clients is surprisingly effective (though it can be expensive on the server side because the server must maintain an active connection to all of its clients). Because it is a useful programming pattern, client-side JavaScript supports it with the EventSource API. To create this kind of long-lived request connection to a web server, simply pass a URL to the EventSource() constructor. When the server writes (properly formatted) data to the connection, the EventSource object translates those into events that you can listen for: This technique for allowing servers to send messages to clients is surprisingly effective (though it can be expensive on the server side because the server must maintain an active connection to all of its clients). Because it is a useful programming pattern, client-side JavaScript supports it with the EventSource API. To create this kind of long-lived request connection to a web server, simply pass a URL to the EventSource() constructor. When the server writes (properly formatted) data to the connection, the EventSource object translates those into events that you can listen for:
...@@ -3075,7 +3075,7 @@ async function broadcastNewMessage(request, response) { ...@@ -3075,7 +3075,7 @@ async function broadcastNewMessage(request, response) {
// Now send this event to all listening clients // Now send this event to all listening clients
clients.forEach(client => client.write(event)); clients.forEach(client => client.write(event));
} }
15.11.3 WebSockets ### 15.11.3 WebSockets
The WebSocket API is a simple interface to a complex and powerful network protocol. WebSockets allow JavaScript code in the browser to easily exchange text and binary messages with a server. As with Server-Sent Events, the client must establish the connection, but once the connection is established, the server can asynchronously send messages to the client. Unlike SSE, binary messages are supported, and messages can be sent in both directions, not just from server to client. The WebSocket API is a simple interface to a complex and powerful network protocol. WebSockets allow JavaScript code in the browser to easily exchange text and binary messages with a server. As with Server-Sent Events, the client must establish the connection, but once the connection is established, the server can asynchronously send messages to the client. Unlike SSE, binary messages are supported, and messages can be sent in both directions, not just from server to client.
The network protocol that enables WebSockets is a kind of extension to HTTP. Although the WebSocket API is reminiscent of low-level network sockets, connection endpoints are not identified by IP address and port. Instead, when you want to connect to a service using the WebSocket protocol, you specify the service with a URL, just as you would for a web service. WebSocket URLs begin with wss:// instead of https://, however. (Browsers typically restrict WebSockets to only work in pages loaded over secure https:// connections). The network protocol that enables WebSockets is a kind of extension to HTTP. Although the WebSocket API is reminiscent of low-level network sockets, connection endpoints are not identified by IP address and port. Instead, when you want to connect to a service using the WebSocket protocol, you specify the service with a URL, just as you would for a web service. WebSocket URLs begin with wss:// instead of https://, however. (Browsers typically restrict WebSockets to only work in pages loaded over secure https:// connections).
...@@ -3127,7 +3127,7 @@ Protocols tend to evolve, however. If a hypothetical stock quotation protocol is ...@@ -3127,7 +3127,7 @@ Protocols tend to evolve, however. If a hypothetical stock quotation protocol is
Anticipating this situation, the WebSocket protocol and API include an application-level protocol negotiation feature. When you call the WebSocket() constructor, the wss:// URL is the first argument, but you can also pass an array of strings as the second argument. If you do this, you are specifying a list of application protocols that you know how to handle and asking the server to pick one. During the connection process, the server will choose one of the protocols (or will fail with an error if it does not support any of the client’s options). Once the connection has been established, the protocol property of the WebSocket object specifies which protocol version the server chose. Anticipating this situation, the WebSocket protocol and API include an application-level protocol negotiation feature. When you call the WebSocket() constructor, the wss:// URL is the first argument, but you can also pass an array of strings as the second argument. If you do this, you are specifying a list of application protocols that you know how to handle and asking the server to pick one. During the connection process, the server will choose one of the protocols (or will fail with an error if it does not support any of the client’s options). Once the connection has been established, the protocol property of the WebSocket object specifies which protocol version the server chose.
15.12 Storage ## 15.12 Storage
Web applications can use browser APIs to store data locally on the user’s computer. This client-side storage serves to give the web browser a memory. Web apps can store user preferences, for example, or even store their complete state, so that they can resume exactly where you left off at the end of your last visit. Client-side storage is segregated by origin, so pages from one site can’t read the data stored by pages from another site. But two pages from the same site can share storage and use it as a communication mechanism. Data input in a form on one page can be displayed in a table on another page, for example. Web applications can choose the lifetime of the data they store: data can be stored temporarily so that it is retained only until the window closes or the browser exits, or it can be saved on the user’s computer and stored permanently so that it is available months or years later. Web applications can use browser APIs to store data locally on the user’s computer. This client-side storage serves to give the web browser a memory. Web apps can store user preferences, for example, or even store their complete state, so that they can resume exactly where you left off at the end of your last visit. Client-side storage is segregated by origin, so pages from one site can’t read the data stored by pages from another site. But two pages from the same site can share storage and use it as a communication mechanism. Data input in a form on one page can be displayed in a table on another page, for example. Web applications can choose the lifetime of the data they store: data can be stored temporarily so that it is retained only until the window closes or the browser exits, or it can be saved on the user’s computer and stored permanently so that it is available months or years later.
There are a number of forms of client-side storage: There are a number of forms of client-side storage:
...@@ -3144,7 +3144,7 @@ IndexedDB is an asynchronous API to an object database that supports indexing. ...@@ -3144,7 +3144,7 @@ IndexedDB is an asynchronous API to an object database that supports indexing.
STORAGE, SECURITY, AND PRIVACY STORAGE, SECURITY, AND PRIVACY
Web browsers often offer to remember web passwords for you, and they store them safely in encrypted form on the device. But none of the forms of client-side data storage described in this chapter involve encryption: you should assume that anything your web applications save resides on the user’s device in unencrypted form. Stored data is therefore accessible to curious users who share access to the device and to malicious software (such as spyware) that exists on the device. For this reason, no form of client-side storage should ever be used for passwords, financial account numbers, or other similarly sensitive information. Web browsers often offer to remember web passwords for you, and they store them safely in encrypted form on the device. But none of the forms of client-side data storage described in this chapter involve encryption: you should assume that anything your web applications save resides on the user’s device in unencrypted form. Stored data is therefore accessible to curious users who share access to the device and to malicious software (such as spyware) that exists on the device. For this reason, no form of client-side storage should ever be used for passwords, financial account numbers, or other similarly sensitive information.
15.12.1 localStorage and sessionStorage ### 15.12.1 localStorage and sessionStorage
The localStorage and sessionStorage properties of the Window object refer to Storage objects. A Storage object behaves much like a regular JavaScript object, except that: The localStorage and sessionStorage properties of the Window object refer to Storage objects. A Storage object behaves much like a regular JavaScript object, except that:
The property values of Storage objects must be strings. The property values of Storage objects must be strings.
...@@ -3216,7 +3216,7 @@ Note that localStorage and the “storage” event can serve as a broadcast mech ...@@ -3216,7 +3216,7 @@ Note that localStorage and the “storage” event can serve as a broadcast mech
As another example, imagine a web-based image-editing application that allows the user to display tool palettes in separate windows. When the user selects a tool, the application uses localStorage to save the current state and to generate a notification to other windows that a new tool has been selected. As another example, imagine a web-based image-editing application that allows the user to display tool palettes in separate windows. When the user selects a tool, the application uses localStorage to save the current state and to generate a notification to other windows that a new tool has been selected.
15.12.2 Cookies ### 15.12.2 Cookies
A cookie is a small amount of named data stored by the web browser and associated with a particular web page or website. Cookies were designed for server-side programming, and at the lowest level, they are implemented as an extension to the HTTP protocol. Cookie data is automatically transmitted between the web browser and web server, so server-side scripts can read and write cookie values that are stored on the client. This section demonstrates how client-side scripts can also manipulate cookies using the cookie property of the Document object. A cookie is a small amount of named data stored by the web browser and associated with a particular web page or website. Cookies were designed for server-side programming, and at the lowest level, they are implemented as an extension to the HTTP protocol. Cookie data is automatically transmitted between the web browser and web server, so server-side scripts can read and write cookie values that are stored on the client. This section demonstrates how client-side scripts can also manipulate cookies using the cookie property of the Document object.
WHY “COOKIE”? WHY “COOKIE”?
...@@ -3290,7 +3290,7 @@ To change the value of a cookie, set its value again using the same name, path, ...@@ -3290,7 +3290,7 @@ To change the value of a cookie, set its value again using the same name, path,
To delete a cookie, set it again using the same name, path, and domain, specifying an arbitrary (or empty) value, and a max-age attribute of 0. To delete a cookie, set it again using the same name, path, and domain, specifying an arbitrary (or empty) value, and a max-age attribute of 0.
15.12.3 IndexedDB ### 15.12.3 IndexedDB
Web application architecture has traditionally featured HTML, CSS, and JavaScript on the client and a database on the server. You may find it surprising, therefore, to learn that the web platform includes a simple object database with a JavaScript API for persistently storing JavaScript objects on the user’s computer and retrieving them as needed. Web application architecture has traditionally featured HTML, CSS, and JavaScript on the client and a database on the server. You may find it surprising, therefore, to learn that the web platform includes a simple object database with a JavaScript API for persistently storing JavaScript objects on the user’s computer and retrieving them as needed.
IndexedDB is an object database, not a relational database, and it is much simpler than databases that support SQL queries. It is more powerful, efficient, and robust than the key/value storage provided by the localStorage, however. Like the localStorage, IndexedDB databases are scoped to the origin of the containing document: two web pages with the same origin can access each other’s data, but web pages from different origins cannot. IndexedDB is an object database, not a relational database, and it is much simpler than databases that support SQL queries. It is more powerful, efficient, and robust than the key/value storage provided by the localStorage, however. Like the localStorage, IndexedDB databases are scoped to the origin of the containing document: two web pages with the same origin can access each other’s data, but web pages from different origins cannot.
...@@ -3436,7 +3436,7 @@ function lookupZipcodes(city, callback) { ...@@ -3436,7 +3436,7 @@ function lookupZipcodes(city, callback) {
request.onsuccess = () => { callback(request.result); }; request.onsuccess = () => { callback(request.result); };
}); });
} }
15.13 Worker Threads and Messaging ## 15.13 Worker Threads and Messaging
One of the fundamental features of JavaScript is that it is single-threaded: a browser will never run two event handlers at the same time, and it will never trigger a timer while an event handler is running, for example. Concurrent updates to application state or to the document are simply not possible, and client-side programmers do not need to think about, or even understand, concurrent programming. A corollary is that client-side JavaScript functions must not run too long; otherwise, they will tie up the event loop and the web browser will become unresponsive to user input. This is the reason that fetch() is an asynchronous function, for example. One of the fundamental features of JavaScript is that it is single-threaded: a browser will never run two event handlers at the same time, and it will never trigger a timer while an event handler is running, for example. Concurrent updates to application state or to the document are simply not possible, and client-side programmers do not need to think about, or even understand, concurrent programming. A corollary is that client-side JavaScript functions must not run too long; otherwise, they will tie up the event loop and the web browser will become unresponsive to user input. This is the reason that fetch() is an asynchronous function, for example.
Web browsers very carefully relax the single-thread requirement with the Worker class: instances of this class represent threads that run concurrently with the main thread and the event loop. Workers live in a self-contained execution environment with a completely independent global object and no access to the Window or Document objects. Workers can communicate with the main thread only through asynchronous message passing. This means that concurrent modifications of the DOM remain impossible, but it also means that you can write long-running functions that do not stall the event loop and hang the browser. Creating a new worker is not a heavyweight operation like opening a new browser window, but workers are not flyweight “fibers” either, and it does not make sense to create new workers to perform trivial operations. Complex web applications may find it useful to create tens of workers, but it is unlikely that an application with hundreds or thousands of workers would be practical. Web browsers very carefully relax the single-thread requirement with the Worker class: instances of this class represent threads that run concurrently with the main thread and the event loop. Workers live in a self-contained execution environment with a completely independent global object and no access to the Window or Document objects. Workers can communicate with the main thread only through asynchronous message passing. This means that concurrent modifications of the DOM remain impossible, but it also means that you can write long-running functions that do not stall the event loop and hang the browser. Creating a new worker is not a heavyweight operation like opening a new browser window, but workers are not flyweight “fibers” either, and it does not make sense to create new workers to perform trivial operations. Complex web applications may find it useful to create tens of workers, but it is unlikely that an application with hundreds or thousands of workers would be practical.
...@@ -3447,7 +3447,7 @@ As with any threading API, there are two parts to the Worker API. The first is t ...@@ -3447,7 +3447,7 @@ As with any threading API, there are two parts to the Worker API. The first is t
The following sections cover Worker and WorkerGlobalScope and also explain the message-passing API that allows workers to communicate with the main thread and each other. The same communication API is used to exchange messages between a document and <iframe> elements contained in the document, and this is covered in the following sections as well. The following sections cover Worker and WorkerGlobalScope and also explain the message-passing API that allows workers to communicate with the main thread and each other. The same communication API is used to exchange messages between a document and <iframe> elements contained in the document, and this is covered in the following sections as well.
15.13.1 Worker Objects ### 15.13.1 Worker Objects
To create a new worker, call the Worker() constructor, passing a URL that specifies the JavaScript code that the worker is to run: To create a new worker, call the Worker() constructor, passing a URL that specifies the JavaScript code that the worker is to run:
let dataCruncher = new Worker("utils/cruncher.js"); let dataCruncher = new Worker("utils/cruncher.js");
...@@ -3466,7 +3466,7 @@ Like all event targets, Worker objects define the standard addEventListener() an ...@@ -3466,7 +3466,7 @@ Like all event targets, Worker objects define the standard addEventListener() an
In addition to postMessage(), Worker objects have just one other method, terminate(), which forces a worker thread to stop running. In addition to postMessage(), Worker objects have just one other method, terminate(), which forces a worker thread to stop running.
15.13.2 The Global Object in Workers ### 15.13.2 The Global Object in Workers
When you create a new worker with the Worker() constructor, you specify the URL of a file of JavaScript code. That code is executed in a new, pristine JavaScript execution environment, isolated from the script that created the worker. The global object for that new execution environment is a WorkerGlobalScope object. A WorkerGlobalScope is something more than the core JavaScript global object, but less than a full-blown client-side Window object. When you create a new worker with the Worker() constructor, you specify the URL of a file of JavaScript code. That code is executed in a new, pristine JavaScript execution environment, isolated from the script that created the worker. The global object for that new execution environment is a WorkerGlobalScope object. A WorkerGlobalScope is something more than the core JavaScript global object, but less than a full-blown client-side Window object.
The WorkerGlobalScope object has a postMessage() method and an onmessage event handler property that are just like those of the Worker object but work in the opposite direction: calling postMessage() inside a worker generates a message event outside the worker, and messages sent from outside the worker are turned into events and delivered to the onmessage handler. Because the WorkerGlobalScope is the global object for a worker, postMessage() and onmessage look like a global function and global variable to worker code. The WorkerGlobalScope object has a postMessage() method and an onmessage event handler property that are just like those of the Worker object but work in the opposite direction: calling postMessage() inside a worker generates a message event outside the worker, and messages sent from outside the worker are turned into events and delivered to the onmessage handler. Because the WorkerGlobalScope is the global object for a worker, postMessage() and onmessage look like a global function and global variable to worker code.
...@@ -3489,7 +3489,7 @@ The usual event target methods addEventListener() and removeEventListener(). ...@@ -3489,7 +3489,7 @@ The usual event target methods addEventListener() and removeEventListener().
Finally, the WorkerGlobalScope object includes important client-side JavaScript APIs including the Console object, the fetch() function, and the IndexedDB API. WorkerGlobalScope also includes the Worker() constructor, which means that worker threads can create their own workers. Finally, the WorkerGlobalScope object includes important client-side JavaScript APIs including the Console object, the fetch() function, and the IndexedDB API. WorkerGlobalScope also includes the Worker() constructor, which means that worker threads can create their own workers.
15.13.3 Importing Code into a Worker ### 15.13.3 Importing Code into a Worker
Workers were defined in web browsers before JavaScript had a module system, so workers have a unique system for including additional code. WorkerGlobalScope defines importScripts() as a global function that all workers have access to: Workers were defined in web browsers before JavaScript had a module system, so workers have a unique system for including additional code. WorkerGlobalScope defines importScripts() as a global function that all workers have access to:
// Before we start working, load the classes and utilities we'll need // Before we start working, load the classes and utilities we'll need
...@@ -3505,7 +3505,7 @@ When a worker loads a module instead of a traditional script, the WorkerGlobalSc ...@@ -3505,7 +3505,7 @@ When a worker loads a module instead of a traditional script, the WorkerGlobalSc
Note that as of early 2020, Chrome is the only browser that supports true modules and import declarations in workers. Note that as of early 2020, Chrome is the only browser that supports true modules and import declarations in workers.
15.13.4 Worker Execution Model ### 15.13.4 Worker Execution Model
Worker threads run their code (and all imported scripts or modules) synchronously from top to bottom, and then enter an asynchronous phase in which they respond to events and timers. If a worker registers a “message” event handler, it will never exit as long as there is a possibility that message events will still arrive. But if a worker doesn’t listen for messages, it will run until there are no further pending tasks (such as fetch() promises and timers) and all task-related callbacks have been called. Once all registered callbacks have been called, there is no way a worker can begin a new task, so it is safe for the thread to exit, which it will do automatically. A worker can also explicitly shut itself down by calling the global close() function. Note that there are no properties or methods on the Worker object that specify whether a worker thread is still running or not, so workers should not close themselves without somehow coordinating this with their parent thread. Worker threads run their code (and all imported scripts or modules) synchronously from top to bottom, and then enter an asynchronous phase in which they respond to events and timers. If a worker registers a “message” event handler, it will never exit as long as there is a possibility that message events will still arrive. But if a worker doesn’t listen for messages, it will run until there are no further pending tasks (such as fetch() promises and timers) and all task-related callbacks have been called. Once all registered callbacks have been called, there is no way a worker can begin a new task, so it is safe for the thread to exit, which it will do automatically. A worker can also explicitly shut itself down by calling the global close() function. Note that there are no properties or methods on the Worker object that specify whether a worker thread is still running or not, so workers should not close themselves without somehow coordinating this with their parent thread.
ERRORS IN WORKERS ERRORS IN WORKERS
...@@ -3524,7 +3524,7 @@ worker.onerror = function(e) { ...@@ -3524,7 +3524,7 @@ worker.onerror = function(e) {
}; };
Like windows, workers can register a handler to be invoked when a Promise is rejected and there is no .catch() function to handle it. Within a worker you can detect this by defining a self.onunhandledrejection function or by using addEventListener() to register a global handler for “unhandledrejection” events. The event object passed to this handler will have a promise property whose value is the Promise object that rejected and a reason property whose value is what would have been passed to a .catch() function. Like windows, workers can register a handler to be invoked when a Promise is rejected and there is no .catch() function to handle it. Within a worker you can detect this by defining a self.onunhandledrejection function or by using addEventListener() to register a global handler for “unhandledrejection” events. The event object passed to this handler will have a promise property whose value is the Promise object that rejected and a reason property whose value is what would have been passed to a .catch() function.
15.13.5 postMessage(), MessagePorts, and MessageChannels ### 15.13.5 postMessage(), MessagePorts, and MessageChannels
The postMessage() method of the Worker object and the global postMesage() function defined inside a worker both work by invoking the postMessage() methods of a pair of MessagePort objects that are automatically created along with the worker. Client-side JavaScript can’t directly access these automatically created MessagePort objects, but it can create new pairs of connected ports with the MessageChannel() constructor: The postMessage() method of the Worker object and the global postMesage() function defined inside a worker both work by invoking the postMessage() methods of a pair of MessagePort objects that are automatically created along with the worker. Client-side JavaScript can’t directly access these automatically created MessagePort objects, but it can create new pairs of connected ports with the MessageChannel() constructor:
let channel = new MessageChannel; // Create a new channel. let channel = new MessageChannel; // Create a new channel.
...@@ -3553,7 +3553,7 @@ MessageChannels are also useful if you create two workers and want to allow them ...@@ -3553,7 +3553,7 @@ MessageChannels are also useful if you create two workers and want to allow them
The other use of the second argument to postMessage() is to transfer ArrayBuffers between workers without having to copy them. This is an important performance enhancement for large ArrayBuffers like those used to hold image data. When an ArrayBuffer is transferred over a MessagePort, the ArrayBuffer becomes unusable in the original thread so that there is no possibility of concurrent access to its contents. If the first argument to postMessage() includes an ArrayBuffer, or any value (such as a typed array) that has an ArrayBuffer, then that buffer may appear as an array element in the second postMessage() argument. If it does appear, then it will be transferred without copying. If not, then the ArrayBuffer will be copied rather than transferred. Example 15-14 will demonstrate the use of this transfer technique with ArrayBuffers. The other use of the second argument to postMessage() is to transfer ArrayBuffers between workers without having to copy them. This is an important performance enhancement for large ArrayBuffers like those used to hold image data. When an ArrayBuffer is transferred over a MessagePort, the ArrayBuffer becomes unusable in the original thread so that there is no possibility of concurrent access to its contents. If the first argument to postMessage() includes an ArrayBuffer, or any value (such as a typed array) that has an ArrayBuffer, then that buffer may appear as an array element in the second postMessage() argument. If it does appear, then it will be transferred without copying. If not, then the ArrayBuffer will be copied rather than transferred. Example 15-14 will demonstrate the use of this transfer technique with ArrayBuffers.
15.13.6 Cross-Origin Messaging with postMessage() ### 15.13.6 Cross-Origin Messaging with postMessage()
There is another use case for the postMessage() method in client-side JavaScript. It involves windows instead of workers, but there are enough similarities between the two cases that we will describe the postMessage() method of the Window object here. There is another use case for the postMessage() method in client-side JavaScript. It involves windows instead of workers, but there are enough similarities between the two cases that we will describe the postMessage() method of the Window object here.
When a document contains an <iframe> element, that element acts as an embedded but independent window. The Element object that represents the <iframe> has a contentWindow property that is the Window object for the embedded document. And for scripts running within that nested iframe, the window.parent property refers to the containing Window object. When two windows display documents with the same origin, then scripts in each of those windows have access to the contents of the other window. But when the documents have different origins, the browser’s same-origin policy prevents JavaScript in one window from accessing the content of another window. When a document contains an <iframe> element, that element acts as an embedded but independent window. The Element object that represents the <iframe> has a contentWindow property that is the Window object for the embedded document. And for scripts running within that nested iframe, the window.parent property refers to the containing Window object. When two windows display documents with the same origin, then scripts in each of those windows have access to the contents of the other window. But when the documents have different origins, the browser’s same-origin policy prevents JavaScript in one window from accessing the content of another window.
...@@ -3564,7 +3564,7 @@ The postMessage() method of a Window is a little different than the postMessage( ...@@ -3564,7 +3564,7 @@ The postMessage() method of a Window is a little different than the postMessage(
JavaScript code running inside a window or <iframe> can receive messages posted to that window or frame by defining the onmessage property of that window or by calling addEventListener() for “message” events. As with workers, when you receive a “message” event for a window, the data property of the event object is the message that was sent. In addition, however, “message” events delivered to windows also define source and origin properties. The source property specifies the Window object that sent the event, and you can use event.source.postMessage() to send a reply. The origin property specifies the origin of the content in the source window. This is not something the sender of the message can forge, and when you receive a “message” event, you will typically want to verify that it is from an origin you expect. JavaScript code running inside a window or <iframe> can receive messages posted to that window or frame by defining the onmessage property of that window or by calling addEventListener() for “message” events. As with workers, when you receive a “message” event for a window, the data property of the event object is the message that was sent. In addition, however, “message” events delivered to windows also define source and origin properties. The source property specifies the Window object that sent the event, and you can use event.source.postMessage() to send a reply. The origin property specifies the origin of the content in the source window. This is not something the sender of the message can forge, and when you receive a “message” event, you will typically want to verify that it is from an origin you expect.
15.14 Example: The Mandelbrot Set ## 15.14 Example: The Mandelbrot Set
This chapter on client-side JavaScript culminates with a long example that demonstrates using workers and messaging to parallelize computationally intensive tasks. But it is written to be an engaging, real-world web application and also demonstrates a number of the other APIs demonstrated in this chapter, including history management; use of the ImageData class with a <canvas>; and the use of keyboard, pointer, and resize events. It also demonstrates important core JavaScript features, including generators and a sophisticated use of Promises. This chapter on client-side JavaScript culminates with a long example that demonstrates using workers and messaging to parallelize computationally intensive tasks. But it is written to be an engaging, real-world web application and also demonstrates a number of the other APIs demonstrated in this chapter, including history management; use of the ImageData class with a <canvas>; and the use of keyboard, pointer, and resize events. It also demonstrates important core JavaScript features, including generators and a sophisticated use of Promises.
The example is a program for displaying and exploring the Mandelbrot set, a complex fractal that includes beautiful images like the one shown in Figure 15-16. The example is a program for displaying and exploring the Mandelbrot set, a complex fractal that includes beautiful images like the one shown in Figure 15-16.
...@@ -4143,7 +4143,7 @@ document.body.style = "margin:0"; // No margin for the <body> ...@@ -4143,7 +4143,7 @@ document.body.style = "margin:0"; // No margin for the <body>
canvas.style.width = "100%"; // Make canvas as wide as body canvas.style.width = "100%"; // Make canvas as wide as body
canvas.style.height = "100%"; // and as high as the body. canvas.style.height = "100%"; // and as high as the body.
new MandelbrotCanvas(canvas); // And start rendering into it! new MandelbrotCanvas(canvas); // And start rendering into it!
15.15 Summary and Suggestions for Further Reading ## 15.15 Summary and Suggestions for Further Reading
This long chapter has covered the fundamentals of client-side JavaScript programming: This long chapter has covered the fundamentals of client-side JavaScript programming:
How scripts and JavaScript modules are included in web pages and how and when they are executed. How scripts and JavaScript modules are included in web pages and how and when they are executed.
...@@ -4172,23 +4172,23 @@ How JavaScript programs can use worker threads to achieve a safe form of concurr ...@@ -4172,23 +4172,23 @@ How JavaScript programs can use worker threads to achieve a safe form of concurr
This has been the longest chapter of the book, by far. But it cannot come close to covering all the APIs available to web browsers. The web platform is sprawling and ever-evolving, and my goal for this chapter was to introduce the most important core APIs. With the knowledge you have from this book, you are well equipped to learn and use new APIs as you need them. But you can’t learn about a new API if you don’t know that it exists, so the short sections that follow end the chapter with a quick list of web platform features that you might want to investigate in the future. This has been the longest chapter of the book, by far. But it cannot come close to covering all the APIs available to web browsers. The web platform is sprawling and ever-evolving, and my goal for this chapter was to introduce the most important core APIs. With the knowledge you have from this book, you are well equipped to learn and use new APIs as you need them. But you can’t learn about a new API if you don’t know that it exists, so the short sections that follow end the chapter with a quick list of web platform features that you might want to investigate in the future.
15.15.1 HTML and CSS ### 15.15.1 HTML and CSS
The web is built upon three key technologies: HTML, CSS, and JavaScript, and knowledge of JavaScript can take you only so far as a web developer unless you also develop your expertise with HTML and CSS. It is important to know how to use JavaScript to manipulate HTML elements and CSS styles, but that knowledge is is much more useful if you also know which HTML elements and which CSS styles to use. The web is built upon three key technologies: HTML, CSS, and JavaScript, and knowledge of JavaScript can take you only so far as a web developer unless you also develop your expertise with HTML and CSS. It is important to know how to use JavaScript to manipulate HTML elements and CSS styles, but that knowledge is is much more useful if you also know which HTML elements and which CSS styles to use.
So before you start exploring more JavaScript APIs, I would encourage you to invest some time in mastering the other tools in a web developer’s toolkit. HTML form and input elements, for example, have sophisticated behavior that is important to understand, and the flexbox and grid layout modes in CSS are incredibly powerful. So before you start exploring more JavaScript APIs, I would encourage you to invest some time in mastering the other tools in a web developer’s toolkit. HTML form and input elements, for example, have sophisticated behavior that is important to understand, and the flexbox and grid layout modes in CSS are incredibly powerful.
Two topics worth paying particular attention to in this area are accessibility (including ARIA attributes) and internationalization (including support for right-to-left writing directions). Two topics worth paying particular attention to in this area are accessibility (including ARIA attributes) and internationalization (including support for right-to-left writing directions).
15.15.2 Performance ### 15.15.2 Performance
Once you have written a web application and released it to the world, the never-ending quest to make it fast begins. It is hard to optimize things that you can’t measure, however, so it is worth familiarizing yourself with the Performance APIs. The performance property of the window object is the main entry point to this API. It includes a high-resolution time source performance.now(), and methods performance.mark() and performance.measure() for marking critical points in your code and measuring the elapsed time between them. Calling these methods creates PerformanceEntry objects that you can access with performance.getEntries(). Browsers add their own PerformanceEntry objects any time the browser loads a new page or fetches a file over the network, and these automatically created PerformanceEntry objects include granular timing details of your application’s network performance. The related PerformanceObserver class allows you to specify a function to be invoked when new PerformanceEntry objects are created. Once you have written a web application and released it to the world, the never-ending quest to make it fast begins. It is hard to optimize things that you can’t measure, however, so it is worth familiarizing yourself with the Performance APIs. The performance property of the window object is the main entry point to this API. It includes a high-resolution time source performance.now(), and methods performance.mark() and performance.measure() for marking critical points in your code and measuring the elapsed time between them. Calling these methods creates PerformanceEntry objects that you can access with performance.getEntries(). Browsers add their own PerformanceEntry objects any time the browser loads a new page or fetches a file over the network, and these automatically created PerformanceEntry objects include granular timing details of your application’s network performance. The related PerformanceObserver class allows you to specify a function to be invoked when new PerformanceEntry objects are created.
15.15.3 Security ### 15.15.3 Security
This chapter introduced the general idea of how to defend against cross-site scripting (XSS) security vulnerabilities in your websites, but we did not go into much detail. The topic of web security is an important one, and you may want to spend some time learning more about it. In addition to XSS, it is worth learning about the Content-Security-Policy HTTP header and understanding how CSP allows you to ask the web browser to restrict the capabilities it grants to JavaScript code. Understanding CORS (Cross-Origin Resource Sharing) is also important. This chapter introduced the general idea of how to defend against cross-site scripting (XSS) security vulnerabilities in your websites, but we did not go into much detail. The topic of web security is an important one, and you may want to spend some time learning more about it. In addition to XSS, it is worth learning about the Content-Security-Policy HTTP header and understanding how CSP allows you to ask the web browser to restrict the capabilities it grants to JavaScript code. Understanding CORS (Cross-Origin Resource Sharing) is also important.
15.15.4 WebAssembly ### 15.15.4 WebAssembly
WebAssembly (or “wasm”) is a low-level virtual machine bytecode format that is designed to integrate well with JavaScript interpreters in web browsers. There are compilers that allow you to compile C, C++, and Rust programs to WebAssembly bytecode and to run those programs in web browsers at close to native speed, without breaking the browser sandbox or security model. WebAssembly can export functions that can be called by JavaScript programs. A typical use case for WebAssembly would be to compile the standard C-language zlib compression library so that JavaScript code has access to high-speed compression and decompression algorithms. Learn more at https://webassembly.org. WebAssembly (or “wasm”) is a low-level virtual machine bytecode format that is designed to integrate well with JavaScript interpreters in web browsers. There are compilers that allow you to compile C, C++, and Rust programs to WebAssembly bytecode and to run those programs in web browsers at close to native speed, without breaking the browser sandbox or security model. WebAssembly can export functions that can be called by JavaScript programs. A typical use case for WebAssembly would be to compile the standard C-language zlib compression library so that JavaScript code has access to high-speed compression and decompression algorithms. Learn more at https://webassembly.org.
15.15.5 More Document and Window Features ### 15.15.5 More Document and Window Features
The Window and Document objects have a number of features that were not covered in this chapter: The Window and Document objects have a number of features that were not covered in this chapter:
The Window object defines alert(), confirm(), and prompt() methods that display simple modal dialogues to the user. These methods block the main thread. The confirm() method synchronously returns a boolean value, and prompt() synchronously returns a string of user input. These are not suitable for production use but can be useful for simple projects and prototypes. The Window object defines alert(), confirm(), and prompt() methods that display simple modal dialogues to the user. These methods block the main thread. The confirm() method synchronously returns a boolean value, and prompt() synchronously returns a string of user input. These are not suitable for production use but can be useful for simple projects and prototypes.
...@@ -4207,7 +4207,7 @@ A MutationObserver allows JavaScript to monitor changes to, or beneath, a specif ...@@ -4207,7 +4207,7 @@ A MutationObserver allows JavaScript to monitor changes to, or beneath, a specif
An IntersectionObserver allows JavaScript to determine which document elements are on the screen and which are close to being on the screen. It is particularly useful for applications that want to dynamically load content on demand as the user scrolls. An IntersectionObserver allows JavaScript to determine which document elements are on the screen and which are close to being on the screen. It is particularly useful for applications that want to dynamically load content on demand as the user scrolls.
15.15.6 Events ### 15.15.6 Events
The sheer number and diversity of events supported by the web platform can be daunting. This chapter has discussed a variety of event types, but here are some more that you may find useful: The sheer number and diversity of events supported by the web platform can be daunting. This chapter has discussed a variety of event types, but here are some more that you may find useful:
Browsers fire “online” and “offline” events at the Window object when the browser gains or loses an internet connection. Browsers fire “online” and “offline” events at the Window object when the browser gains or loses an internet connection.
...@@ -4220,7 +4220,7 @@ The Pointer Lock API enables JavaScript to hide the mouse pointer and get raw mo ...@@ -4220,7 +4220,7 @@ The Pointer Lock API enables JavaScript to hide the mouse pointer and get raw mo
The Gamepad API adds support for game controllers. Use navigator.getGamepads() to get connected Gamepad objects, and listen for “gamepadconnected” events on the Window object to be notified when a new controller is plugged in. The Gamepad object defines an API for querying the current state of the buttons on the controller. The Gamepad API adds support for game controllers. Use navigator.getGamepads() to get connected Gamepad objects, and listen for “gamepadconnected” events on the Window object to be notified when a new controller is plugged in. The Gamepad object defines an API for querying the current state of the buttons on the controller.
15.15.7 Progressive Web Apps and Service Workers ### 15.15.7 Progressive Web Apps and Service Workers
The term Progressive Web Apps, or PWAs, is a buzzword that describes web applications that are built using a few key technologies. Careful documentation of these key technologies would require a book of its own, and I have not covered them in this chapter, but you should be aware of all of these APIs. It is worth noting that powerful modern APIs like these are typically designed to work only on secure HTTPS connections. Websites that are still using http:// URLs will not be able to take advantage of these: The term Progressive Web Apps, or PWAs, is a buzzword that describes web applications that are built using a few key technologies. Careful documentation of these key technologies would require a book of its own, and I have not covered them in this chapter, but you should be aware of all of these APIs. It is worth noting that powerful modern APIs like these are typically designed to work only on secure HTTPS connections. Websites that are still using http:// URLs will not be able to take advantage of these:
A ServiceWorker is a kind of worker thread with the ability to intercept, inspect, and respond to network requests from the web application that it “services.” When a web application registers a service worker, that worker’s code becomes persistent in the browser’s local storage, and when the user visits the associated website again, the service worker is reactivated. Service workers can cache network responses (including files of JavaScript code), which means that web applications that use service workers can effectively install themselves onto the user’s computer for rapid startup and offline use. The Service Worker Cookbook at https://serviceworke.rs is a valuable resource for learning about service workers and their related technologies. A ServiceWorker is a kind of worker thread with the ability to intercept, inspect, and respond to network requests from the web application that it “services.” When a web application registers a service worker, that worker’s code becomes persistent in the browser’s local storage, and when the user visits the associated website again, the service worker is reactivated. Service workers can cache network responses (including files of JavaScript code), which means that web applications that use service workers can effectively install themselves onto the user’s computer for rapid startup and offline use. The Service Worker Cookbook at https://serviceworke.rs is a valuable resource for learning about service workers and their related technologies.
...@@ -4233,7 +4233,7 @@ The Notifications API allows web apps to display notifications using the native ...@@ -4233,7 +4233,7 @@ The Notifications API allows web apps to display notifications using the native
The Push API allows web applications that have a service worker (and that have the user’s permission) to subscribe to notifications from a server, and to display those notifications even when the application itself is not running. Push notifications are common on mobile devices, and the Push API brings web apps closer to feature parity with native apps on mobile. The Push API allows web applications that have a service worker (and that have the user’s permission) to subscribe to notifications from a server, and to display those notifications even when the application itself is not running. Push notifications are common on mobile devices, and the Push API brings web apps closer to feature parity with native apps on mobile.
15.15.8 Mobile Device APIs ### 15.15.8 Mobile Device APIs
There are a number of web APIs that are primarily useful for web apps running on mobile devices. (Unfortunately, a number of these APIs only work on Android devices and not iOS devices.) There are a number of web APIs that are primarily useful for web apps running on mobile devices. (Unfortunately, a number of these APIs only work on Android devices and not iOS devices.)
The Geolocation API allows JavaScript (with the user’s permission) to determine the user’s physical location. It is well supported on desktop and mobile devices, including iOS devices. Use navigator.geolocation.getCurrentPosition() to request the user’s current position and use navigator.geolocation.watchPosition() to register a callback to be called when the user’s position changes. The Geolocation API allows JavaScript (with the user’s permission) to determine the user’s physical location. It is well supported on desktop and mobile devices, including iOS devices. Use navigator.geolocation.getCurrentPosition() to request the user’s current position and use navigator.geolocation.watchPosition() to register a callback to be called when the user’s position changes.
...@@ -4246,19 +4246,19 @@ The “devicemotion” and “deviceorientation” events on the window object r ...@@ -4246,19 +4246,19 @@ The “devicemotion” and “deviceorientation” events on the window object r
The Sensor API is not yet widely supported beyond Chrome on Android devices, but it enables JavaScript access to the full suite of mobile device sensors, including accelerometer, gyroscope, magnetometer, and ambient light sensor. These sensors enable JavaScript to determine which direction a user is facing or to detect when the user shakes their phone, for example. The Sensor API is not yet widely supported beyond Chrome on Android devices, but it enables JavaScript access to the full suite of mobile device sensors, including accelerometer, gyroscope, magnetometer, and ambient light sensor. These sensors enable JavaScript to determine which direction a user is facing or to detect when the user shakes their phone, for example.
15.15.9 Binary APIs ### 15.15.9 Binary APIs
Typed arrays, ArrayBuffers, and the DataView class (all covered in §11.2) enable JavaScript to work with binary data. As described earlier in this chapter, the fetch() API enables JavaScript programs to load binary data over the network. Another source of binary data is files from the user’s local filesystem. For security reasons, JavaScript can’t just read local files. But if the user selects a file for upload (using an <input type="file> form element) or uses drag-and-drop to drop a file into your web application, then JavaScript can access that file as a File object. Typed arrays, ArrayBuffers, and the DataView class (all covered in §11.2) enable JavaScript to work with binary data. As described earlier in this chapter, the fetch() API enables JavaScript programs to load binary data over the network. Another source of binary data is files from the user’s local filesystem. For security reasons, JavaScript can’t just read local files. But if the user selects a file for upload (using an <input type="file> form element) or uses drag-and-drop to drop a file into your web application, then JavaScript can access that file as a File object.
File is a subclass of Blob, and as such, it is an opaque representation of a chunk of data. You can use a FileReader class to asynchronously get the content of a file as an ArrayBuffer or string. (In some browsers, you can skip the FileReader and instead use the Promise-based text() and arrayBuffer() methods defined by the Blob class, or the stream() method for streaming access to the file contents.) File is a subclass of Blob, and as such, it is an opaque representation of a chunk of data. You can use a FileReader class to asynchronously get the content of a file as an ArrayBuffer or string. (In some browsers, you can skip the FileReader and instead use the Promise-based text() and arrayBuffer() methods defined by the Blob class, or the stream() method for streaming access to the file contents.)
When working with binary data, especially streaming binary data, you may need to decode bytes into text or encode text as bytes. The TextEncoder and TextDecoder classes help with this task. When working with binary data, especially streaming binary data, you may need to decode bytes into text or encode text as bytes. The TextEncoder and TextDecoder classes help with this task.
15.15.10 Media APIs ### 15.15.10 Media APIs
The navigator.mediaDevices.getUserMedia() function allows JavaScript to request access to the user’s microphone and/or video camera. A successful request results in a MediaStream object. Video streams can be displayed in a <video> tag (by setting the srcObject property to the stream). Still frames of the video can be captured into an offscreen <canvas> with the canvas drawImage() function resulting in a relatively low-resolution photograph. Audio and video streams returned by getUserMedia() can be recorded and encoded to a Blob with a MediaRecorder object. The navigator.mediaDevices.getUserMedia() function allows JavaScript to request access to the user’s microphone and/or video camera. A successful request results in a MediaStream object. Video streams can be displayed in a <video> tag (by setting the srcObject property to the stream). Still frames of the video can be captured into an offscreen <canvas> with the canvas drawImage() function resulting in a relatively low-resolution photograph. Audio and video streams returned by getUserMedia() can be recorded and encoded to a Blob with a MediaRecorder object.
The more complex WebRTC API enables the transmission and reception of MediaStreams over the network, enabling peer-to-peer video conferencing, for example. The more complex WebRTC API enables the transmission and reception of MediaStreams over the network, enabling peer-to-peer video conferencing, for example.
15.15.11 Cryptography and Related APIs ### 15.15.11 Cryptography and Related APIs
The crypto property of the Window object exposes a getRandomValues() method for cryptographically secure pseudorandom numbers. Other methods for encryption, decryption, key generation, digital signatures, and so on are available through crypto.subtle. The name of this property is a warning to everyone who uses these methods that properly using cryptographic algorithms is difficult and that you should not use those methods unless you really know what you are doing. Also, the methods of crypto.subtle are only available to JavaScript code running within documents that were loaded over a secure HTTPS connection. The crypto property of the Window object exposes a getRandomValues() method for cryptographically secure pseudorandom numbers. Other methods for encryption, decryption, key generation, digital signatures, and so on are available through crypto.subtle. The name of this property is a warning to everyone who uses these methods that properly using cryptographic algorithms is difficult and that you should not use those methods unless you really know what you are doing. Also, the methods of crypto.subtle are only available to JavaScript code running within documents that were loaded over a secure HTTPS connection.
The Credential Management API and the Web Authentication API allow JavaScript to generate, store, and retrieve public key (and other types of) credentials and enables account creation and login without passwords. The JavaScript API consists primarily of the functions navigator.credentials.create() and navigator.credentials.get(), but substantial infrastructure is required on the server side to make these methods work. These APIs are not universally supported yet, but have the potential to revolutionize the way we log in to websites. The Credential Management API and the Web Authentication API allow JavaScript to generate, store, and retrieve public key (and other types of) credentials and enables account creation and login without passwords. The JavaScript API consists primarily of the functions navigator.credentials.create() and navigator.credentials.get(), but substantial infrastructure is required on the server side to make these methods work. These APIs are not universally supported yet, but have the potential to revolutionize the way we log in to websites.
......
...@@ -18,10 +18,10 @@ In addition to the Node executable, a Node installation also includes npm, a pac ...@@ -18,10 +18,10 @@ In addition to the Node executable, a Node installation also includes npm, a pac
Finally, do not overlook the official Node documentation, available at https://nodejs.org/api and https://nodejs.org/docs/guides. I have found it to be well organized and well written. Finally, do not overlook the official Node documentation, available at https://nodejs.org/api and https://nodejs.org/docs/guides. I have found it to be well organized and well written.
16.1 Node Programming Basics ## 16.1 Node Programming Basics
We’ll begin this chapter with a quick look at how Node programs are structured and how they interact with the operating system. We’ll begin this chapter with a quick look at how Node programs are structured and how they interact with the operating system.
16.1.1 Console Output ### 16.1.1 Console Output
If you are used to JavaScript programming for web browsers, one of the minor surprises about Node is that console.log() is not just for debugging, but is Node’s easiest way to display a message to the user, or, more generally, to send output to the stdout stream. Here’s the classic “Hello World” program in Node: If you are used to JavaScript programming for web browsers, one of the minor surprises about Node is that console.log() is not just for debugging, but is Node’s easiest way to display a message to the user, or, more generally, to send output to the stdout stream. Here’s the classic “Hello World” program in Node:
console.log("Hello World!"); console.log("Hello World!");
...@@ -29,7 +29,7 @@ There are lower-level ways to write to stdout, but no fancier or more official w ...@@ -29,7 +29,7 @@ There are lower-level ways to write to stdout, but no fancier or more official w
In web browsers, console.log(), console.warn(), and console.error() typically display little icons next to their output in the developer console to indicate the variety of the log message. Node does not do this, but output displayed with console.error() is distinguished from output displayed with console.log() because console.error() writes to the stderr stream. If you’re using Node to write a program that is designed to have stdout redirected to a file or a pipe, you can use console.error() to display text to the console where the user will see it, even though text printed with console.log() is hidden. In web browsers, console.log(), console.warn(), and console.error() typically display little icons next to their output in the developer console to indicate the variety of the log message. Node does not do this, but output displayed with console.error() is distinguished from output displayed with console.log() because console.error() writes to the stderr stream. If you’re using Node to write a program that is designed to have stdout redirected to a file or a pipe, you can use console.error() to display text to the console where the user will see it, even though text printed with console.log() is hidden.
16.1.2 Command-Line Arguments and Environment Variables ### 16.1.2 Command-Line Arguments and Environment Variables
If you have previously written Unix-style programs designed to be invoked from a terminal or other command-line interface, you know that these programs typically get their input primarily from command-line arguments and secondarily from environment variables. If you have previously written Unix-style programs designed to be invoked from a terminal or other command-line interface, you know that these programs typically get their input primarily from command-line arguments and secondarily from environment variables.
Node follows these Unix conventions. A Node program can read its command-line arguments from the array of strings process.argv. The first element of this array is always the path to the Node executable. The second argument is the path to the file of JavaScript code that Node is executing. Any remaining elements in this array are the space-separated arguments that you passed on the command-line when you invoked Node. Node follows these Unix conventions. A Node program can read its command-line arguments from the array of strings process.argv. The first element of this array is always the path to the Node executable. The second argument is the path to the file of JavaScript code that Node is executing. Any remaining elements in this array are the space-separated arguments that you passed on the command-line when you invoked Node.
...@@ -68,7 +68,7 @@ $ node -p -e 'process.env' ...@@ -68,7 +68,7 @@ $ node -p -e 'process.env'
} }
You can use node -h or node --help to find out what the -p and -e command-line arguments do. However, as a hint, note that you could rewrite the line above as node --eval 'process.env' --print. You can use node -h or node --help to find out what the -p and -e command-line arguments do. However, as a hint, note that you could rewrite the line above as node --eval 'process.env' --print.
16.1.3 Program Life Cycle ### 16.1.3 Program Life Cycle
The node command expects a command-line argument that specifies the file of JavaScript code to be run. This initial file typically imports other modules of JavaScript code, and may also define its own classes and functions. Fundamentally, however, Node executes the JavaScript code in the specified file from top to bottom. Some Node programs exit when they are done executing the last line of code in the file. Often, however, a Node program will keep running long after the initial file has been executed. As we’ll discuss in the following sections, Node programs are often asynchronous and based on callbacks and event handlers. Node programs do not exit until they are done running the initial file and until all callbacks have been called and there are no more pending events. A Node-based server program that listens for incoming network connections will theoretically run forever because it will always be waiting for more events. The node command expects a command-line argument that specifies the file of JavaScript code to be run. This initial file typically imports other modules of JavaScript code, and may also define its own classes and functions. Fundamentally, however, Node executes the JavaScript code in the specified file from top to bottom. Some Node programs exit when they are done executing the last line of code in the file. Often, however, a Node program will keep running long after the initial file has been executed. As we’ll discuss in the following sections, Node programs are often asynchronous and based on callbacks and event handlers. Node programs do not exit until they are done running the initial file and until all callbacks have been called and there are no more pending events. A Node-based server program that listens for incoming network connections will theoretically run forever because it will always be waiting for more events.
A program can force itself to exit by calling process.exit(). Users can usually terminate a Node program by typing Ctrl-C in the terminal window where the program is running. A program can ignore Ctrl-C by registering a signal handler function with process.on("SIGINT", ()=>{}). A program can force itself to exit by calling process.exit(). Users can usually terminate a Node program by typing Ctrl-C in the terminal window where the program is running. A program can ignore Ctrl-C by registering a signal handler function with process.on("SIGINT", ()=>{}).
...@@ -84,7 +84,7 @@ process.on("unhandledRejection", (reason, promise) => { ...@@ -84,7 +84,7 @@ process.on("unhandledRejection", (reason, promise) => {
// reason is whatever value would have been passed to a .catch() function // reason is whatever value would have been passed to a .catch() function
// promise is the Promise object that rejected // promise is the Promise object that rejected
}); });
16.1.4 Node Modules ### 16.1.4 Node Modules
Chapter 10 documented JavaScript module systems, covering both Node modules and ES6 modules. Because Node was created before JavaScript had a module system, Node had to create its own. Node’s module system uses the require() function to import values into a module and the exports object or the module.exports property to export values from a module. These are a fundamental part of the Node programming model, and they are covered in detail in §10.2. Chapter 10 documented JavaScript module systems, covering both Node modules and ES6 modules. Because Node was created before JavaScript had a module system, Node had to create its own. Node’s module system uses the require() function to import values into a module and the exports object or the module.exports property to export values from a module. These are a fundamental part of the Node programming model, and they are covered in detail in §10.2.
Node 13 adds support for standard ES6 modules as well as require-based modules (which Node calls “CommonJS modules”). The two module systems are not fully compatible, so this is somewhat tricky to do. Node needs to know—before it loads a module—whether that module will be using require() and module.exports or if it will be using import and export. When Node loads a file of JavaScript code as a CommonJS module, it automatically defines the require() function along with identifiers exports and module, and it does not enable the import and export keywords. On the other hand, when Node loads a file of code as an ES6 module, it must enable the import and export declarations, and it must not define extra identifiers like require, module, and exports. Node 13 adds support for standard ES6 modules as well as require-based modules (which Node calls “CommonJS modules”). The two module systems are not fully compatible, so this is somewhat tricky to do. Node needs to know—before it loads a module—whether that module will be using require() and module.exports or if it will be using import and export. When Node loads a file of JavaScript code as a CommonJS module, it automatically defines the require() function along with identifiers exports and module, and it does not enable the import and export keywords. On the other hand, when Node loads a file of code as an ES6 module, it must enable the import and export declarations, and it must not define extra identifiers like require, module, and exports.
...@@ -95,7 +95,7 @@ For files that do not have an explicit .mjs or .cjs extension, Node looks for a ...@@ -95,7 +95,7 @@ For files that do not have an explicit .mjs or .cjs extension, Node looks for a
Because there is an enormous amount of existing Node code written using CommonJS module format, Node allows ES6 modules to load CommonJS modules using the import keyword. The reverse is not true, however: a CommonJS module cannot use require() to load an ES6 module. Because there is an enormous amount of existing Node code written using CommonJS module format, Node allows ES6 modules to load CommonJS modules using the import keyword. The reverse is not true, however: a CommonJS module cannot use require() to load an ES6 module.
16.1.5 The Node Package Manager ### 16.1.5 The Node Package Manager
When you install Node, you typically get a program named npm as well. This is the Node Package Manager, and it helps you download and manage libraries that your program depends on. npm keeps track of those dependencies (as well as other information about your program) in a file named package.json in the root directory of your project. This package.json file created by npm is where you would add "type":"module" if you wanted to use ES6 modules for your project. When you install Node, you typically get a program named npm as well. This is the Node Package Manager, and it helps you download and manage libraries that your program depends on. npm keeps track of those dependencies (as well as other information about your program) in a file named package.json in the root directory of your project. This package.json file created by npm is where you would add "type":"module" if you wanted to use ES6 modules for your project.
This chapter does not cover npm in any detail (but see §17.4 for a little more depth). I’m mentioning it here because unless you write programs that do not use any external libraries, you will almost certainly be using npm or a tool like it. Suppose, for example, that you are going to be developing a web server and plan to use the Express framework (https://expressjs.com) to simplify the task. To get started, you might create a directory for your project, and then, in that directory type npm init. npm will ask you for your project name, version number, etc., and will then create an initial package.json file based on your responses. This chapter does not cover npm in any detail (but see §17.4 for a little more depth). I’m mentioning it here because unless you write programs that do not use any external libraries, you will almost certainly be using npm or a tool like it. Suppose, for example, that you are going to be developing a web server and plan to use the Express framework (https://expressjs.com) to simplify the task. To get started, you might create a directory for your project, and then, in that directory type npm init. npm will ask you for your project name, version number, etc., and will then create an initial package.json file based on your responses.
...@@ -112,7 +112,7 @@ added 50 packages from 37 contributors and audited 126 packages in 3.058s ...@@ -112,7 +112,7 @@ added 50 packages from 37 contributors and audited 126 packages in 3.058s
found 0 vulnerabilities found 0 vulnerabilities
When you install a package with npm, npm records this dependency—that your project depends on Express—in the package.json file. With this dependency recorded in package.json, you could give another programmer a copy of your code and your package.json, and they could simply type npm install to automatically download and install all of the libraries that your program needs in order to run. When you install a package with npm, npm records this dependency—that your project depends on Express—in the package.json file. With this dependency recorded in package.json, you could give another programmer a copy of your code and your package.json, and they could simply type npm install to automatically download and install all of the libraries that your program needs in order to run.
16.2 Node Is Asynchronous by Default ## 16.2 Node Is Asynchronous by Default
JavaScript is a general-purpose programming language, so it is perfectly possible to write CPU-intensive programs that multiply large matrices or perform complicated statistical analyses. But Node was designed and optimized for programs—like network servers—that are I/O intensive. And in particular, Node was designed to make it possible to easily implement highly concurrent servers that can handle many requests at the same time. JavaScript is a general-purpose programming language, so it is perfectly possible to write CPU-intensive programs that multiply large matrices or perform complicated statistical analyses. But Node was designed and optimized for programs—like network servers—that are I/O intensive. And in particular, Node was designed to make it possible to easily implement highly concurrent servers that can handle many requests at the same time.
Unlike many programming languages, however, Node does not achieve concurrency with threads. Multithreaded programming is notoriously hard to do correctly, and difficult to debug. Also, threads are a relatively heavyweight abstraction and if you want to write a server that can handle hundreds of concurrent requests, using hundreds of threads may require a prohibitive amount of memory. So Node adopts the single-threaded JavaScript programming model that the web uses, and this turns out to be a vast simplification that makes the creation of network servers a routine skill rather than an arcane one. Unlike many programming languages, however, Node does not achieve concurrency with threads. Multithreaded programming is notoriously hard to do correctly, and difficult to debug. Also, threads are a relatively heavyweight abstraction and if you want to write a server that can handle hundreds of concurrent requests, using hundreds of threads may require a prohibitive amount of memory. So Node adopts the single-threaded JavaScript programming model that the web uses, and this turns out to be a vast simplification that makes the creation of network servers a routine skill rather than an arcane one.
...@@ -187,7 +187,7 @@ This kind of concurrency is often called event-based concurrency. At its core, N ...@@ -187,7 +187,7 @@ This kind of concurrency is often called event-based concurrency. At its core, N
For web servers and other I/O-intensive applications that spend most of their time waiting for input and output, this style of event-based concurrency is efficient and effective. A web server can concurrently handle requests from 50 different clients without needing 50 different threads as long as it uses nonblocking APIs and there is some kind of internal mapping from network sockets to JavaScript functions to invoke when activity occurs on those sockets. For web servers and other I/O-intensive applications that spend most of their time waiting for input and output, this style of event-based concurrency is efficient and effective. A web server can concurrently handle requests from 50 different clients without needing 50 different threads as long as it uses nonblocking APIs and there is some kind of internal mapping from network sockets to JavaScript functions to invoke when activity occurs on those sockets.
16.3 Buffers ## 16.3 Buffers
One of the datatypes you’re likely to use frequently in Node—especially when reading data from files or from the network—is the Buffer class. A Buffer is a lot like a string, except that it is a sequence of bytes instead of a sequence of characters. Node was created before core JavaScript supported typed arrays (see §11.2) and there was no Uint8Array to represent an array of unsigned bytes. Node defined the Buffer class to fill that need. Now that Uint8Array is part of the JavaScript language, Node’s Buffer class is a subclass of Uint8Array. One of the datatypes you’re likely to use frequently in Node—especially when reading data from files or from the network—is the Buffer class. A Buffer is a lot like a string, except that it is a sequence of bytes instead of a sequence of characters. Node was created before core JavaScript supported typed arrays (see §11.2) and there was no Uint8Array to represent an array of unsigned bytes. Node defined the Buffer class to fill that need. Now that Uint8Array is part of the JavaScript language, Node’s Buffer class is a subclass of Uint8Array.
What distinguishes Buffer from its Uint8Array superclass is that it is designed to interoperate with JavaScript strings: the bytes in a buffer can be initialized from character strings or converted to character strings. A character encoding maps each character in some set of characters to an integer. Given a string of text and a character encoding, we can encode the characters in the string into a sequence of bytes. And given a (properly encoded) sequence of bytes and a character encoding, we can decode those bytes into a sequence of characters. Node’s Buffer class has methods that perform both encoding and decoding, and you can recognize these methods because they expect an encoding argument that specifies the encoding to be used. What distinguishes Buffer from its Uint8Array superclass is that it is designed to interoperate with JavaScript strings: the bytes in a buffer can be initialized from character strings or converted to character strings. A character encoding maps each character in some set of characters to an integer. Given a string of text and a character encoding, we can encode the characters in the string into a sequence of bytes. And given a (properly encoded) sequence of bytes and a character encoding, we can decode those bytes into a sequence of characters. Node’s Buffer class has methods that perform both encoding and decoding, and you can recognize these methods because they expect an encoding argument that specifies the encoding to be used.
...@@ -238,7 +238,7 @@ dead.readBigUInt64BE(6) // => 0xBEEFDEADBEEFDEADn ...@@ -238,7 +238,7 @@ dead.readBigUInt64BE(6) // => 0xBEEFDEADBEEFDEADn
dead.readUInt32LE(1020) // => 0xEFBEADDE dead.readUInt32LE(1020) // => 0xEFBEADDE
If you write a Node program that actually manipulates binary data, you may find yourself using the Buffer class extensively. On the other hand, if you are just working with text that is read from or written to a file or the network, then you may only encounter Buffer as an intermediate representation of your data. A number of Node APIs can take input or return output as either strings or Buffer objects. Typically, if you pass a string, or expect a string to be returned, from one of these APIs, you’ll need to specify the name of the text encoding you want to use. And if you do this, then you may not need to use a Buffer object at all. If you write a Node program that actually manipulates binary data, you may find yourself using the Buffer class extensively. On the other hand, if you are just working with text that is read from or written to a file or the network, then you may only encounter Buffer as an intermediate representation of your data. A number of Node APIs can take input or return output as either strings or Buffer objects. Typically, if you pass a string, or expect a string to be returned, from one of these APIs, you’ll need to specify the name of the text encoding you want to use. And if you do this, then you may not need to use a Buffer object at all.
16.4 Events and EventEmitter ## 16.4 Events and EventEmitter
As described, all of Node’s APIs are asynchronous by default. For many of them, this asynchrony takes the form of two-argument error-first callbacks that are invoked when the requested operation is complete. But some of the more complicated APIs are event-based instead. This is typically the case when the API is designed around an object rather than a function, or when a callback function needs to be invoked multiple times, or when there are multiple types of callback functions that may be required. Consider the net.Server class, for example: an object of this type is a server socket that is used to accept incoming connections from clients. It emits a “listening” event when it first starts listening for connections, a “connection” event every time a client connects, and a “close” event when it has been closed and is no longer listening. As described, all of Node’s APIs are asynchronous by default. For many of them, this asynchrony takes the form of two-argument error-first callbacks that are invoked when the requested operation is complete. But some of the more complicated APIs are event-based instead. This is typically the case when the API is designed around an object rather than a function, or when a callback function needs to be invoked multiple times, or when there are multiple types of callback functions that may be required. Consider the net.Server class, for example: an object of this type is a server socket that is used to accept incoming connections from clients. It emits a “listening” event when it first starts listening for connections, a “connection” event every time a client connects, and a “close” event when it has been closed and is no longer listening.
In Node, objects that emit events are instances of EventEmitter or a subclass of EventEmitter: In Node, objects that emit events are instances of EventEmitter or a subclass of EventEmitter:
...@@ -269,7 +269,7 @@ Any value returned by an event handler function is ignored. If an event handler ...@@ -269,7 +269,7 @@ Any value returned by an event handler function is ignored. If an event handler
Recall that Node’s callback-based APIs use error-first callbacks, and it is important that you always check the first callback argument to see if an error occurred. With event-based APIs, the equivalent is “error” events. Since event-based APIs are often used for networking and other forms of streaming I/O, they are vulnerable to unpredictable asynchronous errors, and most EventEmitters define an “error” event that they emit when an error occurs. Whenever you use an event-based API, you should make it a habit to register a handler for “error” events. “Error” events get special treatment by the EventEmitter class. If emit() is called to emit an “error” event, and if there are no handlers registered for that event type, then an exception will be thrown. Since this occurs asynchronously, there is no way for you to handle the exception in a catch block, so this kind of error typically causes your program to exit. Recall that Node’s callback-based APIs use error-first callbacks, and it is important that you always check the first callback argument to see if an error occurred. With event-based APIs, the equivalent is “error” events. Since event-based APIs are often used for networking and other forms of streaming I/O, they are vulnerable to unpredictable asynchronous errors, and most EventEmitters define an “error” event that they emit when an error occurs. Whenever you use an event-based API, you should make it a habit to register a handler for “error” events. “Error” events get special treatment by the EventEmitter class. If emit() is called to emit an “error” event, and if there are no handlers registered for that event type, then an exception will be thrown. Since this occurs asynchronously, there is no way for you to handle the exception in a catch block, so this kind of error typically causes your program to exit.
16.5 Streams ## 16.5 Streams
When implementing an algorithm to process data, it is almost always easiest to read all the data into memory, do the processing, and then write the data out. For example, you could write a Node function to copy a file like this.1 When implementing an algorithm to process data, it is almost always easiest to read all the data into memory, do the processing, and then write the data out. For example, you could write a Node function to copy a file like this.1
const fs = require("fs"); const fs = require("fs");
...@@ -312,7 +312,7 @@ The need to coordinate stream readability (buffer not empty) and writability (bu ...@@ -312,7 +312,7 @@ The need to coordinate stream readability (buffer not empty) and writability (bu
The subsections that follow demonstrate how to read and write from Node’s stream classes. The subsections that follow demonstrate how to read and write from Node’s stream classes.
16.5.1 Pipes ### 16.5.1 Pipes
Sometimes, you need to read data from a stream simply to turn around and write that same data to another stream. Imagine, for example, that you are writing a simple HTTP server that serves a directory of static files. In this case, you will need to read data from a file input stream and write it out to a network socket. But instead of writing your own code to handle the reading and writing, you can instead simply connect the two sockets together as a “pipe” and let Node handle the complexities for you. Simply pass the Writable stream to the pipe() method of the Readable stream: Sometimes, you need to read data from a stream simply to turn around and write that same data to another stream. Imagine, for example, that you are writing a simple HTTP server that serves a directory of static files. In this case, you will need to read data from a file input stream and write it out to a network socket. But instead of writing your own code to handle the reading and writing, you can instead simply connect the two sockets together as a “pipe” and let Node handle the complexities for you. Simply pass the Writable stream to the pipe() method of the Readable stream:
const fs = require("fs"); const fs = require("fs");
...@@ -416,7 +416,7 @@ process.stdin // Start with standard input, ...@@ -416,7 +416,7 @@ process.stdin // Start with standard input,
.pipe(new GrepStream(pattern)) // pipe it to our GrepStream, .pipe(new GrepStream(pattern)) // pipe it to our GrepStream,
.pipe(process.stdout) // and pipe that to standard out. .pipe(process.stdout) // and pipe that to standard out.
.on("error", () => process.exit()); // Exit gracefully if stdout closes. .on("error", () => process.exit()); // Exit gracefully if stdout closes.
16.5.2 Asynchronous Iteration ### 16.5.2 Asynchronous Iteration
In Node 12 and later, Readable streams are asynchronous iterators, which means that within an async function you can use a for/await loop to read string or Buffer chunks from a stream using code that is structured like synchronous code would be. (See §13.4 for more on asynchronous iterators and for/await loops.) In Node 12 and later, Readable streams are asynchronous iterators, which means that within an async function you can use a for/await loop to read string or Buffer chunks from a stream using code that is structured like synchronous code would be. (See §13.4 for more on asynchronous iterators and for/await loops.)
Using an asynchronous iterator is almost as easy as using the pipe() method, and is probably easier when you need to process each chunk you read in some way. Here’s how we could rewrite the grep program in the previous section using an async function and a for/await loop: Using an asynchronous iterator is almost as easy as using the pipe() method, and is probably easier when you need to process each chunk you read in some way. Here’s how we could rewrite the grep program in the previous section using an async function and a for/await loop:
...@@ -460,7 +460,7 @@ grep(process.stdin, process.stdout, pattern) // Call the async grep() function. ...@@ -460,7 +460,7 @@ grep(process.stdin, process.stdout, pattern) // Call the async grep() function.
console.error(err); console.error(err);
process.exit(); process.exit();
}); });
16.5.3 Writing to Streams and Handling Backpressure ### 16.5.3 Writing to Streams and Handling Backpressure
The async grep() function in the preceding code example demonstrated how to use a Readable stream as an asynchronous iterator, but it also demonstrated that you can write data to a Writable stream simply by passing it to the write() method. The write() method takes a buffer or string as the first argument. (Object streams expect other kinds of objects, but are beyond the scope of this chapter.) If you pass a buffer, the bytes of that buffer will be written directly. If you pass a string, it will be encoded to a buffer of bytes before being written. Writable streams have a default encoding that is used when you pass a string as the only argument to write(). The default encoding is typically “utf8,” but you can set it explicitly by calling setDefaultEncoding() on the Writable stream. Alternatively, when you pass a string as the first argument to write() you can pass an encoding name as the second argument. The async grep() function in the preceding code example demonstrated how to use a Readable stream as an asynchronous iterator, but it also demonstrated that you can write data to a Writable stream simply by passing it to the write() method. The write() method takes a buffer or string as the first argument. (Object streams expect other kinds of objects, but are beyond the scope of this chapter.) If you pass a buffer, the bytes of that buffer will be written directly. If you pass a string, it will be encoded to a buffer of bytes before being written. Writable streams have a default encoding that is used when you pass a string as the only argument to write(). The default encoding is typically “utf8,” but you can set it explicitly by calling setDefaultEncoding() on the Writable stream. Alternatively, when you pass a string as the first argument to write() you can pass an encoding name as the second argument.
write() optionally takes a callback function as its third argument. This will be invoked when the data has actually been written and is no longer in the Writable stream’s internal buffer. (This callback may also be invoked if an error occurs, but this is not guaranteed. You should register an “error” event handler on the Writable stream to detect errors.) write() optionally takes a callback function as its third argument. This will be invoked when the data has actually been written and is no longer in the Writable stream’s internal buffer. (This callback may also be invoked if an error occurs, but this is not guaranteed. You should register an “error” event handler on the Writable stream to detect errors.)
...@@ -519,7 +519,7 @@ async function copy(source, destination) { ...@@ -519,7 +519,7 @@ async function copy(source, destination) {
copy(process.stdin, process.stdout); copy(process.stdin, process.stdout);
Before we conclude this discussion of writing to streams, note again that failing to respond to backpressure can cause your program to use more memory than it should when the internal buffer of a Writable stream overflows and grows larger and larger. If you are writing a network server, this can be a remotely exploitable security issue. Suppose you write an HTTP server that delivers files over the network, but you didn’t use pipe() and you didn’t take the time to handle backpressure from the write() method. An attacker could write an HTTP client that initiates requests for large files (such as images) but never actually reads the body of the request. Since the client is not reading the data over the network, and the server isn’t responding to backpressure, buffers on the server are going to overflow. With enough concurrent connections from the attacker, this can turn into a denial-of-service attack that slows your server down or even crashes it. Before we conclude this discussion of writing to streams, note again that failing to respond to backpressure can cause your program to use more memory than it should when the internal buffer of a Writable stream overflows and grows larger and larger. If you are writing a network server, this can be a remotely exploitable security issue. Suppose you write an HTTP server that delivers files over the network, but you didn’t use pipe() and you didn’t take the time to handle backpressure from the write() method. An attacker could write an HTTP client that initiates requests for large files (such as images) but never actually reads the body of the request. Since the client is not reading the data over the network, and the server isn’t responding to backpressure, buffers on the server are going to overflow. With enough concurrent connections from the attacker, this can turn into a denial-of-service attack that slows your server down or even crashes it.
16.5.4 Reading Streams with Events ### 16.5.4 Reading Streams with Events
Node’s readable streams have two modes, each of which has its own API for reading. If you can’t use pipes or asynchronous iteration in your program, you will need to pick one of these two event-based APIs for handling streams. It is important that you use only one or the other and do not mix the two APIs. Node’s readable streams have two modes, each of which has its own API for reading. If you can’t use pipes or asynchronous iteration in your program, you will need to pick one of these two event-based APIs for handling streams. It is important that you use only one or the other and do not mix the two APIs.
FLOWING MODE FLOWING MODE
...@@ -614,7 +614,7 @@ sha256(process.argv[2], (err, hash) => { // Pass filename from command line. ...@@ -614,7 +614,7 @@ sha256(process.argv[2], (err, hash) => { // Pass filename from command line.
console.log(hash); // print the hash string. console.log(hash); // print the hash string.
} }
}); });
16.6 Process, CPU, and Operating System Details ## 16.6 Process, CPU, and Operating System Details
The global Process object has a number of useful properties and functions that generally relate to the state of the currently running Node process. Consult the Node documentation for complete details, but here are some properties and functions you should be aware of: The global Process object has a number of useful properties and functions that generally relate to the state of the currently running Node process. Consult the Node documentation for complete details, but here are some properties and functions you should be aware of:
process.argv // An array of command-line arguments. process.argv // An array of command-line arguments.
...@@ -663,12 +663,12 @@ os.totalmem() // Returns the total amount of RAM in bytes. ...@@ -663,12 +663,12 @@ os.totalmem() // Returns the total amount of RAM in bytes.
os.type() // Returns OS: "Linux", "Darwin", or "Windows_NT", e.g. os.type() // Returns OS: "Linux", "Darwin", or "Windows_NT", e.g.
os.uptime() // Returns the system uptime in seconds. os.uptime() // Returns the system uptime in seconds.
os.userInfo() // Returns uid, username, home, and shell of current user. os.userInfo() // Returns uid, username, home, and shell of current user.
16.7 Working with Files ## 16.7 Working with Files
Node’s “fs” module is a comprehensive API for working with files and directories. It is complemented by the “path” module, which defines utility functions for working with file and directory names. The “fs” module contains a handful of high-level functions for easily reading, writing, and copying files. But most of the functions in the module are low-level JavaScript bindings to Unix system calls (and their equivalents on Windows). If you have worked with low-level filesystem calls before (in C or other languages), then the Node API will be familiar to you. If not, you may find parts of the “fs” API to be terse and unintuitive. The function to delete a file, for example, is called unlink(). Node’s “fs” module is a comprehensive API for working with files and directories. It is complemented by the “path” module, which defines utility functions for working with file and directory names. The “fs” module contains a handful of high-level functions for easily reading, writing, and copying files. But most of the functions in the module are low-level JavaScript bindings to Unix system calls (and their equivalents on Windows). If you have worked with low-level filesystem calls before (in C or other languages), then the Node API will be familiar to you. If not, you may find parts of the “fs” API to be terse and unintuitive. The function to delete a file, for example, is called unlink().
The “fs” module defines a large API, mainly because there are usually multiple variants of each fundamental operation. As discussed at the beginning of the chapter, most functions such as fs.readFile() are nonblocking, callback-based, and asynchronous. Typically, though, each of these functions has a synchronous blocking variant, such as fs.readFileSync(). In Node 10 and later, many of these functions also have a Promise-based asynchronous variant such as fs.promises.readFile(). Most “fs” functions take a string as their first argument, specifying the path (filename plus optional directory names) to the file that is to be operated on. But a number of these functions also support a variant that takes an integer “file descriptor” as the first argument instead of a path. These variants have names that begin with the letter “f.” For example, fs.truncate() truncates a file specified by path, and fs.ftruncate() truncates a file specified by file descriptor. There is a Promise-based fs.promises.truncate() that expects a path and another Promise-based version that is implemented as a method of a FileHandle object. (The FileHandle class is the equivalent of a file descriptor in the Promise-based API.) Finally, there are a handful of functions in the “fs” module that have variants whose names are prefixed with the letter “l.” These “l” variants are like the base function but do not follow symbolic links in the filesystem and instead operate directly on the symbolic links themselves. The “fs” module defines a large API, mainly because there are usually multiple variants of each fundamental operation. As discussed at the beginning of the chapter, most functions such as fs.readFile() are nonblocking, callback-based, and asynchronous. Typically, though, each of these functions has a synchronous blocking variant, such as fs.readFileSync(). In Node 10 and later, many of these functions also have a Promise-based asynchronous variant such as fs.promises.readFile(). Most “fs” functions take a string as their first argument, specifying the path (filename plus optional directory names) to the file that is to be operated on. But a number of these functions also support a variant that takes an integer “file descriptor” as the first argument instead of a path. These variants have names that begin with the letter “f.” For example, fs.truncate() truncates a file specified by path, and fs.ftruncate() truncates a file specified by file descriptor. There is a Promise-based fs.promises.truncate() that expects a path and another Promise-based version that is implemented as a method of a FileHandle object. (The FileHandle class is the equivalent of a file descriptor in the Promise-based API.) Finally, there are a handful of functions in the “fs” module that have variants whose names are prefixed with the letter “l.” These “l” variants are like the base function but do not follow symbolic links in the filesystem and instead operate directly on the symbolic links themselves.
16.7.1 Paths, File Descriptors, and FileHandles ### 16.7.1 Paths, File Descriptors, and FileHandles
In order to use the “fs” module to work with files, you first need to be able to name the file you want to work with. Files are most often specified by path, which means the name of the file itself, plus the hierarchy of directories in which the file appears. If a path is absolute, it means that directories all the way up to the filesystem root are specified. Otherwise, the path is relative and is only meaningful in relation to some other path, usually the current working directory. Working with paths can be a little tricky because different operating systems use different characters to separate directory names, it is easy to accidentally double those separator characters when concatenating paths, and because ../ parent directory path segments need special handling. Node’s “path” module and a couple of other important Node features help: In order to use the “fs” module to work with files, you first need to be able to name the file you want to work with. Files are most often specified by path, which means the name of the file itself, plus the hierarchy of directories in which the file appears. If a path is absolute, it means that directories all the way up to the filesystem root are specified. Otherwise, the path is relative and is only meaningful in relation to some other path, usually the current working directory. Working with paths can be a little tricky because different operating systems use different characters to separate directory names, it is easy to accidentally double those separator characters when concatenating paths, and because ../ parent directory path segments need special handling. Node’s “path” module and a couple of other important Node features help:
// Some important paths // Some important paths
...@@ -712,7 +712,7 @@ Some of the “fs” functions we’ll be covering in the next sections expect a ...@@ -712,7 +712,7 @@ Some of the “fs” functions we’ll be covering in the next sections expect a
Finally, in the Promise-based API defined by fs.promises, the equivalent of fs.open() is fs.promises.open(), which returns a Promise that resolves to a FileHandle object. This FileHandle object serves the same purpose as a file descriptor. Again, however, unless you need to use the lowest-level read() and write() methods of a FileHandle, there is really no reason to create one. And if you do create a FileHandle, you should remember to call its close() method once you are done with it. Finally, in the Promise-based API defined by fs.promises, the equivalent of fs.open() is fs.promises.open(), which returns a Promise that resolves to a FileHandle object. This FileHandle object serves the same purpose as a file descriptor. Again, however, unless you need to use the lowest-level read() and write() methods of a FileHandle, there is really no reason to create one. And if you do create a FileHandle, you should remember to call its close() method once you are done with it.
16.7.2 Reading Files ### 16.7.2 Reading Files
Node allows you to read file content all at once, via a stream, or with the low-level API. Node allows you to read file content all at once, via a stream, or with the low-level API.
If your files are small, or if memory usage and performance are not the highest priority, then it is often easiest to read the entire content of a file with a single call. You can do this synchronously, with a callback, or with a Promise. By default, you’ll get the bytes of the file as a buffer, but if you specify an encoding, you’ll get a decoded string instead. If your files are small, or if memory usage and performance are not the highest priority, then it is often easiest to read the entire content of a file with a single call. You can do this synchronously, with a callback, or with a Promise. By default, you’ll get the bytes of the file as a buffer, but if you specify an encoding, you’ll get a decoded string instead.
...@@ -798,7 +798,7 @@ function readData(filename) { ...@@ -798,7 +798,7 @@ function readData(filename) {
fs.closeSync(fd); fs.closeSync(fd);
} }
} }
16.7.3 Writing Files ### 16.7.3 Writing Files
Writing files in Node is a lot like reading them, with a few extra details that you need to know about. One of these details is that the way you create a new file is simply by writing to a filename that does not already exist. Writing files in Node is a lot like reading them, with a few extra details that you need to know about. One of these details is that the way you create a new file is simply by writing to a filename that does not already exist.
As with reading, there are three basic ways to write files in Node. If you have the entire content of the file in a string or a buffer, you can write the entire thing in one call with fs.writeFile() (callback-based), fs.writeFileSync() (synchronous), or fs.promises.writeFile() (Promise-based): As with reading, there are three basic ways to write files in Node. If you have the entire content of the file in a string or a buffer, you can write the entire thing in one call with fs.writeFile() (callback-based), fs.writeFileSync() (synchronous), or fs.promises.writeFile() (Promise-based):
...@@ -854,7 +854,7 @@ You can chop off the end of a file with fs.truncate(), fs.truncateSync(), or fs. ...@@ -854,7 +854,7 @@ You can chop off the end of a file with fs.truncate(), fs.truncateSync(), or fs.
The various file-writing functions described here return or invoke their callback or resolve their Promise when the data has been “written” in the sense that Node has handed it off to the operating system. But this does not necessarily mean that the data has actually been written to persistent storage yet: at least some of your data may still be buffered somewhere in the operating system or in a device driver waiting to be written to disk. If you call fs.writeSync() to synchronously write some data to a file, and if there is a power outage immediately after the function returns, you may still lose data. If you want to force your data out to disk so you know for sure that it has been safely saved, use fs.fsync() or fs.fsyncSync(). These functions only work with file descriptors: there is no path-based version. The various file-writing functions described here return or invoke their callback or resolve their Promise when the data has been “written” in the sense that Node has handed it off to the operating system. But this does not necessarily mean that the data has actually been written to persistent storage yet: at least some of your data may still be buffered somewhere in the operating system or in a device driver waiting to be written to disk. If you call fs.writeSync() to synchronously write some data to a file, and if there is a power outage immediately after the function returns, you may still lose data. If you want to force your data out to disk so you know for sure that it has been safely saved, use fs.fsync() or fs.fsyncSync(). These functions only work with file descriptors: there is no path-based version.
16.7.4 File Operations ### 16.7.4 File Operations
The preceding discussion of Node’s stream classes included two examples of copyFile() functions. These are not practical utilities that you would actually use because the “fs” module defines its own fs.copyFile() method (and also fs.copyFileSync() and fs.promises.copyFile(), of course). The preceding discussion of Node’s stream classes included two examples of copyFile() functions. These are not practical utilities that you would actually use because the “fs” module defines its own fs.copyFile() method (and also fs.copyFileSync() and fs.promises.copyFile(), of course).
These functions take the name of the original file and the name of the copy as their first two arguments. These can be specified as strings or as URL or Buffer objects. An optional third argument is an integer whose bits specify flags that control details of the copy operation. And for the callback-based fs.copyFile(), the final argument is a callback function that will be called with no arguments when the copy is complete, or that will be called with an error argument if something fails. Following are some examples: These functions take the name of the original file and the name of the copy as their first two arguments. These can be specified as strings or as URL or Buffer objects. An optional third argument is an integer whose bits specify flags that control details of the copy operation. And for the callback-based fs.copyFile(), the final argument is a callback function that will be called with no arguments when the copy is complete, or that will be called with an error argument if something fails. Following are some examples:
...@@ -893,7 +893,7 @@ The functions fs.link() and fs.symlink() and their variants have the same signat ...@@ -893,7 +893,7 @@ The functions fs.link() and fs.symlink() and their variants have the same signat
Finally, fs.unlink(), fs.unlinkSync(), and fs.promises.unlink() are Node’s functions for deleting a file. (The unintuitive naming is inherited from Unix where deleting a file is basically the opposite of creating a hard link to it.) Call this function with the string, buffer, or URL path to the file to be deleted, and pass a callback if you are using the callback-based version: Finally, fs.unlink(), fs.unlinkSync(), and fs.promises.unlink() are Node’s functions for deleting a file. (The unintuitive naming is inherited from Unix where deleting a file is basically the opposite of creating a hard link to it.) Call this function with the string, buffer, or URL path to the file to be deleted, and pass a callback if you are using the callback-based version:
fs.unlinkSync("backups/ch15.bak"); fs.unlinkSync("backups/ch15.bak");
16.7.5 File Metadata ### 16.7.5 File Metadata
The fs.stat(), fs.statSync(), and fs.promises.stat() functions allow you to obtain metadata for a specified file or directory. For example: The fs.stat(), fs.statSync(), and fs.promises.stat() functions allow you to obtain metadata for a specified file or directory. For example:
const fs = require("fs"); const fs = require("fs");
...@@ -921,7 +921,7 @@ fs.chown(), fs.lchown(), and fs.fchown() (along with synchronous and Promise-bas ...@@ -921,7 +921,7 @@ fs.chown(), fs.lchown(), and fs.fchown() (along with synchronous and Promise-bas
Finally, you can set the access time and modification time of a file or directory with fs.utimes() and fs.futimes() and their variants. Finally, you can set the access time and modification time of a file or directory with fs.utimes() and fs.futimes() and their variants.
16.7.6 Working with Directories ### 16.7.6 Working with Directories
To create a new directory in Node, use fs.mkdir(), fs.mkdirSync(), or fs.promises.mkdir(). The first argument is the path of the directory to be created. The optional second argument can be an integer that specifies the mode (permissions bits) for the new directory. Or you can pass an object with optional mode and recursive properties. If recursive is true, then this function will create any directories in the path that do not already exist: To create a new directory in Node, use fs.mkdir(), fs.mkdirSync(), or fs.promises.mkdir(). The first argument is the path of the directory to be created. The optional second argument can be an integer that specifies the mode (permissions bits) for the new directory. Or you can pass an object with optional mode and recursive properties. If recursive is true, then this function will create any directories in the path that do not already exist:
// Ensure that dist/ and dist/lib/ both exist. // Ensure that dist/ and dist/lib/ both exist.
...@@ -972,7 +972,7 @@ async function listDirectory(dirpath) { ...@@ -972,7 +972,7 @@ async function listDirectory(dirpath) {
console.log(String(size).padStart(10), name); console.log(String(size).padStart(10), name);
} }
} }
16.8 HTTP Clients and Servers ## 16.8 HTTP Clients and Servers
Node’s “http,” “https,” and “http2” modules are full-featured but relatively low-level implementations of the HTTP protocols. They define comprehensive APIs for implementing HTTP clients and servers. Because the APIs are relatively low-level, there is not room in this chapter to cover all the features. But the examples that follow demonstrate how to write basic clients and servers. Node’s “http,” “https,” and “http2” modules are full-featured but relatively low-level implementations of the HTTP protocols. They define comprehensive APIs for implementing HTTP clients and servers. Because the APIs are relatively low-level, there is not room in this chapter to cover all the features. But the examples that follow demonstrate how to write basic clients and servers.
The simplest way to make a basic HTTP GET request is with http.get() or https.get(). The first argument to these functions is the URL to fetch. (If it is an http:// URL, you must use the “http” module, and if it is an https:// URL you must use the “https” module.) The second argument is a callback that will be invoked with an IncomingMessage object when the server’s response has started to arrive. When the callback is called, the HTTP status and headers are available, but the body may not be ready yet. The IncomingMessage object is a Readable stream, and you can use the techniques demonstrated earlier in this chapter to read the response body from it. The simplest way to make a basic HTTP GET request is with http.get() or https.get(). The first argument to these functions is the URL to fetch. (If it is an http:// URL, you must use the “http” module, and if it is an https:// URL you must use the “https” module.) The second argument is a callback that will be invoked with an IncomingMessage object when the server’s response has started to arrive. When the callback is called, the HTTP status and headers are available, but the body may not be ready yet. The IncomingMessage object is a Readable stream, and you can use the techniques demonstrated earlier in this chapter to read the response body from it.
...@@ -1166,7 +1166,7 @@ function serve(rootDirectory, port) { ...@@ -1166,7 +1166,7 @@ function serve(rootDirectory, port) {
serve(process.argv[2] || "/tmp", parseInt(process.argv[3]) || 8000); serve(process.argv[2] || "/tmp", parseInt(process.argv[3]) || 8000);
Node’s built-in modules are all you need to write simple HTTP and HTTPS servers. Note, however, that production servers are not typically built directly on top of these modules. Instead, most nontrivial servers are implemented using external libraries—such as the Express framework—that provide “middleware” and other higher-level utilities that backend web developers have come to expect. Node’s built-in modules are all you need to write simple HTTP and HTTPS servers. Note, however, that production servers are not typically built directly on top of these modules. Instead, most nontrivial servers are implemented using external libraries—such as the Express framework—that provide “middleware” and other higher-level utilities that backend web developers have come to expect.
16.9 Non-HTTP Network Servers and Clients ## 16.9 Non-HTTP Network Servers and Clients
Web servers and clients have become so ubiquitous that it is easy to forget that it is possible to write clients and servers that do not use HTTP. Even though Node has a reputation as a good environment for writing web servers, Node also has full support for writing other types of network servers and clients. Web servers and clients have become so ubiquitous that it is easy to forget that it is possible to write clients and servers that do not use HTTP. Even though Node has a reputation as a good environment for writing web servers, Node also has full support for writing other types of network servers and clients.
If you are comfortable working with streams, then networking is relatively simple, because network sockets are simply a kind of Duplex stream. The “net” module defines Server and Socket classes. To create a server, call net.createServer(), then call the listen() method of the resulting object to tell the server what port to listen on for connections. The Server object will generate “connection” events when a client connects on that port, and the value passed to the event listener will be a Socket object. The Socket object is a Duplex stream, and you can use it to read data from the client and write data to the client. Call end() on the Socket to disconnect. If you are comfortable working with streams, then networking is relatively simple, because network sockets are simply a kind of Duplex stream. The “net” module defines Server and Socket classes. To create a server, call net.createServer(), then call the listen() method of the resulting object to tell the server what port to listen on for connections. The Server object will generate “connection” events when a client connects on that port, and the value passed to the event listener will be a Socket object. The Socket object is a Duplex stream, and you can use it to read data from the client and write data to the client. Call end() on the Socket to disconnect.
...@@ -1272,10 +1272,10 @@ process.stdin.pipe(socket); // Pipe data from stdin to the socket ...@@ -1272,10 +1272,10 @@ process.stdin.pipe(socket); // Pipe data from stdin to the socket
socket.on("close", () => process.exit()); // Quit when the socket closes. socket.on("close", () => process.exit()); // Quit when the socket closes.
In addition to supporting TCP-based servers, Node’s “net” module also supports interprocess communication over “Unix domain sockets” that are identified by a filesystem path rather than by a port number. We are not going to cover that kind of socket in this chapter, but the Node documentation has details. Other Node features that we don’t have space to cover here include the “dgram” module for UDP-based clients and servers and the “tls” module that is to “net” as “https” is to “http.” The tls.Server and tls.TLSSocket classes allow the creation of TCP servers (like the knock-knock joke server) that use SSL-encrypted connections like HTTPS servers do. In addition to supporting TCP-based servers, Node’s “net” module also supports interprocess communication over “Unix domain sockets” that are identified by a filesystem path rather than by a port number. We are not going to cover that kind of socket in this chapter, but the Node documentation has details. Other Node features that we don’t have space to cover here include the “dgram” module for UDP-based clients and servers and the “tls” module that is to “net” as “https” is to “http.” The tls.Server and tls.TLSSocket classes allow the creation of TCP servers (like the knock-knock joke server) that use SSL-encrypted connections like HTTPS servers do.
16.10 Working with Child Processes ## 16.10 Working with Child Processes
In addition to writing highly concurrent servers, Node also works well for writing scripts that execute other programs. In Node the “child_process” module defines a number of functions for running other programs as child processes. This section demonstrates some of those functions, starting with the simplest and moving to the more complicated. In addition to writing highly concurrent servers, Node also works well for writing scripts that execute other programs. In Node the “child_process” module defines a number of functions for running other programs as child processes. This section demonstrates some of those functions, starting with the simplest and moving to the more complicated.
16.10.1 execSync() and execFileSync() ### 16.10.1 execSync() and execFileSync()
The easiest way to run another program is with child_process.execSync(). This function takes the command to run as its first argument. It creates a child process, runs a shell in that process, and uses the shell to execute the command you passed. Then it blocks until the command (and the shell) exit. If the command exits with an error, then execSync() throws an exception. Otherwise, execSync() returns whatever output the command writes to its stdout stream. By default this return value is a buffer, but you can specify an encoding in an optional second argument to get a string instead. If the command writes any output to stderr, that output just gets passed through to the parent process’s stderr stream. The easiest way to run another program is with child_process.execSync(). This function takes the command to run as its first argument. It creates a child process, runs a shell in that process, and uses the shell to execute the command you passed. Then it blocks until the command (and the shell) exit. If the command exits with an error, then execSync() throws an exception. Otherwise, execSync() returns whatever output the command writes to its stdout stream. By default this return value is a buffer, but you can specify an encoding in an optional second argument to get a string instead. If the command writes any output to stderr, that output just gets passed through to the parent process’s stderr stream.
So, for example, if you are writing a script and performance is not a concern, you might use child_process.execSync() to list a directory with a familiar Unix shell command rather than using the fs.readdirSync() function: So, for example, if you are writing a script and performance is not a concern, you might use child_process.execSync() to list a directory with a familiar Unix shell command rather than using the fs.readdirSync() function:
...@@ -1305,7 +1305,7 @@ timeout specifies the maximum number of milliseconds that the child process shou ...@@ -1305,7 +1305,7 @@ timeout specifies the maximum number of milliseconds that the child process shou
uid specifies the user ID (a number) under which the program should be run. If the parent process is running in a privileged account, it can use this option to run the child with reduced privileges. uid specifies the user ID (a number) under which the program should be run. If the parent process is running in a privileged account, it can use this option to run the child with reduced privileges.
16.10.2 exec() and execFile() ### 16.10.2 exec() and execFile()
The execSync() and execFileSync() functions are, as their names indicate, synchronous: they block and do not return until the child process exits. Using these functions is a lot like typing Unix commands in a terminal window: they allow you to run a sequence of commands one at a time. But if you’re writing a program that needs to accomplish a number of tasks, and those tasks don’t depend on each other in any way, then you may want to parallelize them and run multiple commands at the same time. You can do this with the asynchronous functions child_process.exec() and child_process.execFile(). The execSync() and execFileSync() functions are, as their names indicate, synchronous: they block and do not return until the child process exits. Using these functions is a lot like typing Unix commands in a terminal window: they allow you to run a sequence of commands one at a time. But if you’re writing a program that needs to accomplish a number of tasks, and those tasks don’t depend on each other in any way, then you may want to parallelize them and run multiple commands at the same time. You can do this with the asynchronous functions child_process.exec() and child_process.execFile().
exec() and execFile() are like their synchronous variants except that they return immediately with a ChildProcess object that represents the running child process, and they take an error-first callback as their final argument. The callback is invoked when the child process exits, and it is actually called with three arguments. The first is the error, if any; it will be null if the process terminated normally. The second argument is the collected output that was sent to the child’s standard output stream. And the third argument is any output that was sent to the child’s standard error stream. exec() and execFile() are like their synchronous variants except that they return immediately with a ChildProcess object that represents the running child process, and they take an error-first callback as their final argument. The callback is invoked when the child process exits, and it is actually called with three arguments. The first is the error, if any; it will be null if the process terminated normally. The second argument is the collected output that was sent to the child’s standard output stream. And the third argument is any output that was sent to the child’s standard error stream.
...@@ -1329,7 +1329,7 @@ function parallelExec(commands) { ...@@ -1329,7 +1329,7 @@ function parallelExec(commands) {
} }
module.exports = parallelExec; module.exports = parallelExec;
16.10.3 spawn() ### 16.10.3 spawn()
The various exec functions described so far—both synchronous and asynchronous—are designed to be used with child processes that run quickly and do not produce a lot of output. Even the asynchronous exec() and execFile() are nonstreaming: they return the process output in a single batch, only after the process has exited. The various exec functions described so far—both synchronous and asynchronous—are designed to be used with child processes that run quickly and do not produce a lot of output. Even the asynchronous exec() and execFile() are nonstreaming: they return the process output in a single batch, only after the process has exited.
The child_process.spawn() function allows you streaming access to the output of the child process, while the process is still running. It also allows you to write data to the child process (which will see that data as input on its standard input stream): this means it is possible to dynamically interact with a child process, sending it input based on the output it generates. The child_process.spawn() function allows you streaming access to the output of the child process, while the process is still running. It also allows you to write data to the child process (which will see that data as input on its standard input stream): this means it is possible to dynamically interact with a child process, sending it input based on the output it generates.
...@@ -1342,7 +1342,7 @@ Similarly, the stdin property of the ChildProcess object is a Writeable stream: ...@@ -1342,7 +1342,7 @@ Similarly, the stdin property of the ChildProcess object is a Writeable stream:
The ChildProcess object also defines a pid property that specifies the process id of the child. And it defines a kill() method that you can use to terminate a child process. The ChildProcess object also defines a pid property that specifies the process id of the child. And it defines a kill() method that you can use to terminate a child process.
16.10.4 fork() ### 16.10.4 fork()
child_process.fork() is a specialized function for running a module of JavaScript code in a child Node process. fork() expects the same arguments as spawn(), but the first argument should specify the path to a file of JavaScript code instead of an executable binary file. child_process.fork() is a specialized function for running a module of JavaScript code in a child Node process. fork() expects the same arguments as spawn(), but the first argument should specify the path to a file of JavaScript code instead of an executable binary file.
A child process created with fork() can communicate with the parent process via its standard input and standard output streams, as described in the previous section for spawn(). But in addition, fork() enables another, much easier, communication channel between the parent and child processes. A child process created with fork() can communicate with the parent process via its standard input and standard output streams, as described in the previous section for spawn(). But in addition, fork() enables another, much easier, communication channel between the parent and child processes.
...@@ -1379,7 +1379,7 @@ Starting child processes is an expensive operation, and the child process would ...@@ -1379,7 +1379,7 @@ Starting child processes is an expensive operation, and the child process would
The first argument to send() will be serialized with JSON.stringify() and deserialized in the child process with JSON.parse(), so you should only include values that are supported by the JSON format. send() has a special second argument, however, that allows you to transfer Socket and Server objects (from the “net” module) to a child process. Network servers tend to be IO-bound rather than compute-bound, but if you have written a server that needs to do more computation than a single CPU can handle, and if you’re running that server on a machine with multiple CPUs, then you could use fork() to create multiple child processes for handling requests. In the parent process, you might listen for “connection” events on your Server object, then get the Socket object from that “connection” event and send() it—using the special second argument—to one of the child processes to be handled. (Note that this is an unlikely solution to an uncommon scenario. Rather than writing a server that forks child processes, it is probably simpler to keep your server single-threaded and deploy multiple instances of it in production to handle the load.) The first argument to send() will be serialized with JSON.stringify() and deserialized in the child process with JSON.parse(), so you should only include values that are supported by the JSON format. send() has a special second argument, however, that allows you to transfer Socket and Server objects (from the “net” module) to a child process. Network servers tend to be IO-bound rather than compute-bound, but if you have written a server that needs to do more computation than a single CPU can handle, and if you’re running that server on a machine with multiple CPUs, then you could use fork() to create multiple child processes for handling requests. In the parent process, you might listen for “connection” events on your Server object, then get the Socket object from that “connection” event and send() it—using the special second argument—to one of the child processes to be handled. (Note that this is an unlikely solution to an uncommon scenario. Rather than writing a server that forks child processes, it is probably simpler to keep your server single-threaded and deploy multiple instances of it in production to handle the load.)
16.11 Worker Threads ## 16.11 Worker Threads
As explained at the beginning of this chapter, Node’s concurrency model is single-threaded and event-based. But in version 10 and later, Node does allow true multithreaded programming, with an API that closely mirrors the Web Workers API defined by web browsers (§15.13). Multithreaded programming has a well-deserved reputation for being difficult. This is almost entirely because of the need to carefully synchronize access by threads to shared memory. But JavaScript threads (in both Node and browsers) do not share memory by default, so the dangers and difficulties of using threads do not apply to these “workers” in JavaScript. As explained at the beginning of this chapter, Node’s concurrency model is single-threaded and event-based. But in version 10 and later, Node does allow true multithreaded programming, with an API that closely mirrors the Web Workers API defined by web browsers (§15.13). Multithreaded programming has a well-deserved reputation for being difficult. This is almost entirely because of the need to carefully synchronize access by threads to shared memory. But JavaScript threads (in both Node and browsers) do not share memory by default, so the dangers and difficulties of using threads do not apply to these “workers” in JavaScript.
Instead of using shared memory, JavaScript’s worker threads communicate by message passing. The main thread can send a message to a worker thread by calling the postMessage() method of the Worker object that represents that thread. The worker thread can receive messages from its parent by listening for “message” events. And workers can send messages to the main thread with their own version of postMessage(), which the parent can receive with its own “message” event handler. The example code will make it clear how this works. Instead of using shared memory, JavaScript’s worker threads communicate by message passing. The main thread can send a message to a worker thread by calling the postMessage() method of the Worker object that represents that thread. The worker thread can receive messages from its parent by listening for “message” events. And workers can send messages to the main thread with their own version of postMessage(), which the parent can receive with its own “message” event handler. The example code will make it clear how this works.
...@@ -1394,7 +1394,7 @@ In general, workers allow us to turn blocking synchronous operations into nonblo ...@@ -1394,7 +1394,7 @@ In general, workers allow us to turn blocking synchronous operations into nonblo
Worker threads are not nearly as heavyweight as child processes, but they are not lightweight. It does not generally make sense to create a worker unless you have significant work for it to do. And, generally speaking, if your program is not CPU-bound and is not having responsiveness problems, then you probably do not need worker threads. Worker threads are not nearly as heavyweight as child processes, but they are not lightweight. It does not generally make sense to create a worker unless you have significant work for it to do. And, generally speaking, if your program is not CPU-bound and is not having responsiveness problems, then you probably do not need worker threads.
16.11.1 Creating Workers and Passing Messages ### 16.11.1 Creating Workers and Passing Messages
The Node module that defines workers is known as “worker_threads.” In this section we’ll refer to it with the identifier threads: The Node module that defines workers is known as “worker_threads.” In this section we’ll refer to it with the identifier threads:
const threads = require("worker_threads"); const threads = require("worker_threads");
...@@ -1458,7 +1458,7 @@ Node makes a copy of the object passed to postMessage() rather than sharing it d ...@@ -1458,7 +1458,7 @@ Node makes a copy of the object passed to postMessage() rather than sharing it d
The structured clone algorithm enables serialization of most JavaScript types, including Map, Set, Date, and RegExp objects and typed arrays, but it cannot, in general, copy types defined by the Node host environment, such as sockets and streams. Note, however, that Buffer objects are partially supported: if you pass a Buffer to postMessage() it will be received as a Uint8Array, and can be converted back into a Buffer with Buffer.from(). Read more about the structured clone algorithm in “The Structured Clone Algorithm”. The structured clone algorithm enables serialization of most JavaScript types, including Map, Set, Date, and RegExp objects and typed arrays, but it cannot, in general, copy types defined by the Node host environment, such as sockets and streams. Note, however, that Buffer objects are partially supported: if you pass a Buffer to postMessage() it will be received as a Uint8Array, and can be converted back into a Buffer with Buffer.from(). Read more about the structured clone algorithm in “The Structured Clone Algorithm”.
16.11.2 The Worker Execution Environment ### 16.11.2 The Worker Execution Environment
For the most part, JavaScript code in a Node worker thread runs just like it would in Node’s main thread. There are a few differences that you should be aware of, and some of these differences involve properties of the optional second argument to the Worker() constructor: For the most part, JavaScript code in a Node worker thread runs just like it would in Node’s main thread. There are a few differences that you should be aware of, and some of these differences involve properties of the optional second argument to the Worker() constructor:
As we’ve seen, threads.isMainThread is true in the main thread but is always false in any worker thread. As we’ve seen, threads.isMainThread is true in the main thread but is always false in any worker thread.
...@@ -1479,7 +1479,7 @@ Worker threads are not allowed to change shared state of the process they are pa ...@@ -1479,7 +1479,7 @@ Worker threads are not allowed to change shared state of the process they are pa
Operating system signals (like SIGINT and SIGTERM) are only delivered to the main thread; they cannot be received or handled in worker threads. Operating system signals (like SIGINT and SIGTERM) are only delivered to the main thread; they cannot be received or handled in worker threads.
16.11.3 Communication Channels and MessagePorts ### 16.11.3 Communication Channels and MessagePorts
When a new worker thread is created, a communication channel is created along with it that allows messages to be passed back and forth between the worker and the parent thread. As we’ve seen, the worker thread uses threads.parentPort to send and receive messages to and from the parent thread, and the parent thread uses the Worker object to send and receive messages to and from the worker thread. When a new worker thread is created, a communication channel is created along with it that allows messages to be passed back and forth between the worker and the parent thread. As we’ve seen, the worker thread uses threads.parentPort to send and receive messages to and from the parent thread, and the parent thread uses the Worker object to send and receive messages to and from the worker thread.
The worker thread API also allows the creation of custom communication channels using the MessageChannel API defined by web browsers and covered in §15.13.5. If you have read that section, much of what follows will sound familiar to you. The worker thread API also allows the creation of custom communication channels using the MessageChannel API defined by web browsers and covered in §15.13.5. If you have read that section, much of what follows will sound familiar to you.
...@@ -1496,7 +1496,7 @@ You can also call close() on either port to break the connection between the two ...@@ -1496,7 +1496,7 @@ You can also call close() on either port to break the connection between the two
Note that the code example above creates a pair of MessagePort objects and then uses those objects to transmit a message within the main thread. In order to use custom communication channels with workers, we must transfer one of the two ports from the thread in which it is created to the thread in which it will be used. The next section explains how to do this. Note that the code example above creates a pair of MessagePort objects and then uses those objects to transmit a message within the main thread. In order to use custom communication channels with workers, we must transfer one of the two ports from the thread in which it is created to the thread in which it will be used. The next section explains how to do this.
16.11.4 Transferring MessagePorts and Typed Arrays ### 16.11.4 Transferring MessagePorts and Typed Arrays
The postMessage() function uses the structured clone algorithm, and as we’ve noted, it cannot copy objects like SSockets and Streams. It can handle MessagePort objects, but only as a special case using a special technique. The postMessage() method (of a Worker object, of threads.parentPort, or of any MessagePort object) takes an optional second argument. This argument (called transferList) is an array of objects that are to be transferred between threads rather than being copied. The postMessage() function uses the structured clone algorithm, and as we’ve noted, it cannot copy objects like SSockets and Streams. It can handle MessagePort objects, but only as a special case using a special technique. The postMessage() method (of a Worker object, of threads.parentPort, or of any MessagePort object) takes an optional second argument. This argument (called transferList) is an array of objects that are to be transferred between threads rather than being copied.
A MessagePort object cannot be copied by the structured clone algorithm, but it can be transferred. If the first argument to postMessage() has included one or more MessagePorts (nested arbitrarily deeply within the Message object), then those MessagePort objects must also appear as members of the array passed as the second argument. Doing this tells Node that it does not need to make a copy of the MessagePort, and can instead just give the existing object to the other thread. The key thing to understand, however, about transferring values between threads is that once a value is transferred, it can no longer be used in the thread that called postMessage(). A MessagePort object cannot be copied by the structured clone algorithm, but it can be transferred. If the first argument to postMessage() has included one or more MessagePorts (nested arbitrarily deeply within the Message object), then those MessagePort objects must also appear as members of the array passed as the second argument. Doing this tells Node that it does not need to make a copy of the MessagePort, and can instead just give the existing object to the other thread. The key thing to understand, however, about transferring values between threads is that once a value is transferred, it can no longer be used in the thread that called postMessage().
...@@ -1530,7 +1530,7 @@ let pixels = new Uint32Array(1024*1024); // 4 megabytes of memory ...@@ -1530,7 +1530,7 @@ let pixels = new Uint32Array(1024*1024); // 4 megabytes of memory
worker.postMessage(pixels, [ pixels.buffer ]); worker.postMessage(pixels, [ pixels.buffer ]);
As with transferred MessagePorts, a transferred typed array becomes unusable once transferred. No exceptions are thrown if you attempt to use a MessagePort or typed array that has been transferred; these objects simply stop doing anything when you interact with them. As with transferred MessagePorts, a transferred typed array becomes unusable once transferred. No exceptions are thrown if you attempt to use a MessagePort or typed array that has been transferred; these objects simply stop doing anything when you interact with them.
16.11.5 Sharing Typed Arrays Between Threads ### 16.11.5 Sharing Typed Arrays Between Threads
In addition to transferring typed arrays between threads, it is actually possible to share a typed array between threads. Simply create a SharedArrayBuffer of the desired size and then use that buffer to create a typed array. When a typed array that is backed by a SharedArrayBuffer is passed via postMessage(), the underlying memory will be shared between the threads. You should not include the shared buffer in the second argument to postMessage() in this case. In addition to transferring typed arrays between threads, it is actually possible to share a typed array between threads. Simply create a SharedArrayBuffer of the desired size and then use that buffer to create a typed array. When a typed array that is backed by a SharedArrayBuffer is passed via postMessage(), the underlying memory will be shared between the threads. You should not include the shared buffer in the second argument to postMessage() in this case.
You really should not do this, however, because JavaScript was never designed with thread safety in mind and multithreaded programming is very difficult to get right. (And this is why SharedArrayBuffer was not covered in §11.2: it is a niche feature that is difficult to get right.) Even the simple ++ operator is not thread-safe because it needs to read a value, increment it, and write it back. If two threads are incrementing a value at the same time, it will often only be incremented once, as the following code demonstrates: You really should not do this, however, because JavaScript was never designed with thread safety in mind and multithreaded programming is very difficult to get right. (And this is why SharedArrayBuffer was not covered in §11.2: it is a niche feature that is difficult to get right.) Even the simple ++ operator is not thread-safe because it needs to read a value, increment it, and write it back. If two threads are incrementing a value at the same time, it will often only be incremented once, as the following code demonstrates:
...@@ -1603,7 +1603,7 @@ if (threads.isMainThread) { ...@@ -1603,7 +1603,7 @@ if (threads.isMainThread) {
} }
This new version of the code correctly prints the number 20,000,000. But it is about nine times slower than the incorrect code it replaces. It would be much simpler and much faster to just do all 20 million increments in one thread. Also note that atomic operations may be able to ensure thread safety for image-processing algorithms for which each array element is a value entirely independent of all other values. But in most real-world programs, multiple array elements are often related to one another and some kind of higher-level thread synchronization is required. The low-level Atomics.wait() and Atomics.notify() function can help with this, but a discussion of their use is out of scope for this book. This new version of the code correctly prints the number 20,000,000. But it is about nine times slower than the incorrect code it replaces. It would be much simpler and much faster to just do all 20 million increments in one thread. Also note that atomic operations may be able to ensure thread safety for image-processing algorithms for which each array element is a value entirely independent of all other values. But in most real-world programs, multiple array elements are often related to one another and some kind of higher-level thread synchronization is required. The low-level Atomics.wait() and Atomics.notify() function can help with this, but a discussion of their use is out of scope for this book.
16.12 Summary ## 16.12 Summary
Although JavaScript was created to run in web browsers, Node has made JavaScript into a general-purpose programming language. It is particularly popular for implementing web servers, but its deep bindings to the operating system mean that it is also a good alternative to shell scripts. Although JavaScript was created to run in web browsers, Node has made JavaScript into a general-purpose programming language. It is particularly popular for implementing web servers, but its deep bindings to the operating system mean that it is also a good alternative to shell scripts.
The most important topics covered in this long chapter include: The most important topics covered in this long chapter include:
......
...@@ -21,7 +21,7 @@ The Flow language extension (or the similar TypeScript extension) that allows yo ...@@ -21,7 +21,7 @@ The Flow language extension (or the similar TypeScript extension) that allows yo
This chapter does not document these tools and extensions in any comprehensive way. The goal is simply to explain them in enough depth that you can understand why they are useful and when you might want to use them. Everything covered in this chapter is widely used in the JavaScript programming world, and if you do decide to adopt a tool or extension, you’ll find lots of documentation and tutorials online. This chapter does not document these tools and extensions in any comprehensive way. The goal is simply to explain them in enough depth that you can understand why they are useful and when you might want to use them. Everything covered in this chapter is widely used in the JavaScript programming world, and if you do decide to adopt a tool or extension, you’ll find lots of documentation and tutorials online.
17.1 Linting with ESLint ## 17.1 Linting with ESLint
In programming, the term lint refers to code that, while technically correct, is unsightly, or a possible bug, or suboptimal in some way. A linter is a tool for detecting lint in your code, and linting is the process of running a linter on your code (and then fixing your code to remove the lint so that the linter no longer complains). In programming, the term lint refers to code that, while technically correct, is unsightly, or a possible bug, or suboptimal in some way. A linter is a tool for detecting lint in your code, and linting is the process of running a linter on your code (and then fixing your code to remove the lint so that the linter no longer complains).
The most commonly used linter for JavaScript today is ESLint. If you run it and then take the time to actually fix the issues it points out, it will make your code cleaner and less likely to have bugs. Consider the following code: The most commonly used linter for JavaScript today is ESLint. If you run it and then take the time to actually fix the issues it points out, it will make your code cleaner and less likely to have bugs. Consider the following code:
...@@ -53,7 +53,7 @@ Linters can seem nitpicky sometimes. Does it really matter whether we used doubl ...@@ -53,7 +53,7 @@ Linters can seem nitpicky sometimes. Does it really matter whether we used doubl
ESLint defines many linting rules and has an ecosystem of plug-ins that add many more. But ESLint is fully configurable, and you can define a configuration file that tunes ESLint to enforce exactly the rules you want and only those rules. ESLint defines many linting rules and has an ecosystem of plug-ins that add many more. But ESLint is fully configurable, and you can define a configuration file that tunes ESLint to enforce exactly the rules you want and only those rules.
17.2 JavaScript Formatting with Prettier ## 17.2 JavaScript Formatting with Prettier
One of the reasons that some projects use linters is to enforce a consistent coding style so that when a team of programmers is working on a shared codebase, they use compatible code conventions. This includes code indentation rules, but can also include things like what kind of quotation marks are preferred and whether there should be a space between the for keyword and the open parenthesis that follows it. One of the reasons that some projects use linters is to enforce a consistent coding style so that when a team of programmers is working on a shared codebase, they use compatible code conventions. This includes code indentation rules, but can also include things like what kind of quotation marks are preferred and whether there should be a space between the for keyword and the open parenthesis that follows it.
A modern alternative to enforcing code formatting rules via a linter is to adopt a tool like Prettier to automatically parse and reformat all of your code. A modern alternative to enforcing code formatting rules via a linter is to adopt a tool like Prettier to automatically parse and reformat all of your code.
...@@ -83,7 +83,7 @@ Prettier is configurable, but it only has a few options. You can select the maxi ...@@ -83,7 +83,7 @@ Prettier is configurable, but it only has a few options. You can select the maxi
Personally, I really like using Prettier on JavaScript projects. I have not used it for the code in this book, however, because in much of my code I rely on careful hand formatting to align my comments vertically, and Prettier messes them up. Personally, I really like using Prettier on JavaScript projects. I have not used it for the code in this book, however, because in much of my code I rely on careful hand formatting to align my comments vertically, and Prettier messes them up.
17.3 Unit Testing with Jest ## 17.3 Unit Testing with Jest
Writing tests is an important part of any nontrivial programming project. Dynamic languages like JavaScript support testing frameworks that dramatically reduce the effort required to write tests, and almost make test writing fun! There are a lot of test tools and libraries for JavaScript, and many are written in a modular way so that it is possible to pick one library as your test runner, another library for assertions, and a third for mocking. In this section, however, we’ll describe Jest, which is a popular framework that includes everything you need in a single package. Writing tests is an important part of any nontrivial programming project. Dynamic languages like JavaScript support testing frameworks that dramatically reduce the effort required to write tests, and almost make test writing fun! There are a lot of test tools and libraries for JavaScript, and many are written in a modular way so that it is possible to pick one library as your test runner, another library for assertions, and a third for mocking. In this section, however, we’ll describe Jest, which is a popular framework that includes everything you need in a single package.
Suppose you’ve written the following function: Suppose you’ve written the following function:
...@@ -191,7 +191,7 @@ Time: 1.508s ...@@ -191,7 +191,7 @@ Time: 1.508s
Ran all test suites matching /getTemperature/i. Ran all test suites matching /getTemperature/i.
Running our test gave us 100% code coverage for the module we were testing, which is exactly what we wanted. It only gave us partial coverage of getJSON(), but we mocked that module and were not trying to test it, so that is expected. Running our test gave us 100% code coverage for the module we were testing, which is exactly what we wanted. It only gave us partial coverage of getJSON(), but we mocked that module and were not trying to test it, so that is expected.
17.4 Package Management with npm ## 17.4 Package Management with npm
In modern software development, it is common for any nontrivial program that you write to depend on third-party software libraries. If you’re writing a web server in Node, for example, you might be using the Express framework. And if you’re creating a user interface to be displayed in a web browser, you might use a frontend framework like React or LitElement or Angular. A package manager makes it easy to find and install third-party packages like these. Just as importantly, a package manager keeps track of what packages your code depends on and saves this information into a file so that when someone else wants to try your program, they can download your code and your list of dependencies, then use their own package manager to install all the third-party packages that your code needs. In modern software development, it is common for any nontrivial program that you write to depend on third-party software libraries. If you’re writing a web server in Node, for example, you might be using the Express framework. And if you’re creating a user interface to be displayed in a web browser, you might use a frontend framework like React or LitElement or Angular. A package manager makes it easy to find and install third-party packages like these. Just as importantly, a package manager keeps track of what packages your code depends on and saves this information into a file so that when someone else wants to try your program, they can download your code and your list of dependencies, then use their own package manager to install all the third-party packages that your code needs.
npm is the package manager that is bundled with Node, and was introduced in §16.1.5. It is just as useful for client-side JavaScript programming as it is for server-side programming with Node, however. npm is the package manager that is bundled with Node, and was introduced in §16.1.5. It is just as useful for client-side JavaScript programming as it is for server-side programming with Node, however.
...@@ -231,7 +231,7 @@ When you install a tool like ESLint locally for a project, the eslint script win ...@@ -231,7 +231,7 @@ When you install a tool like ESLint locally for a project, the eslint script win
The company behind npm also maintains the https://npmjs.com package repository, which holds hundreds of thousands of open source packages. But you don’t have to use the npm package manager to access this repository of packages. Alternatives include yarn and pnpm. The company behind npm also maintains the https://npmjs.com package repository, which holds hundreds of thousands of open source packages. But you don’t have to use the npm package manager to access this repository of packages. Alternatives include yarn and pnpm.
17.5 Code Bundling ## 17.5 Code Bundling
If you are writing a large JavaScript program to run in web browsers, you will probably want to use a code-bundling tool, especially if you use external libraries that are delivered as modules. Web developers have been using ES6 modules (§10.3) for years, since well before the import and export keywords were supported on the web. In order to do this, programmers use a code-bundler tool that starts at the main entry point (or entry points) of the program and follows the tree of import directives to find all modules that the program depends on. It then combines all of those individual module files into a single bundle of JavaScript code and rewrites the import and export directives to make the code work in this new form. The result is a single file of code that can be loaded into a web browser that does not support modules. If you are writing a large JavaScript program to run in web browsers, you will probably want to use a code-bundling tool, especially if you use external libraries that are delivered as modules. Web developers have been using ES6 modules (§10.3) for years, since well before the import and export keywords were supported on the web. In order to do this, programmers use a code-bundler tool that starts at the main entry point (or entry points) of the program and follows the tree of import directives to find all modules that the program depends on. It then combines all of those individual module files into a single bundle of JavaScript code and rewrites the import and export directives to make the code work in this new form. The result is a single file of code that can be loaded into a web browser that does not support modules.
ES6 modules are nearly universally supported by web browsers today, but web developers still tend to use code bundlers, at least when releasing production code. Developers find that user experience is best when a single medium-sized bundle of code is loaded when a user first visits a website than when many small modules are loaded. ES6 modules are nearly universally supported by web browsers today, but web developers still tend to use code bundlers, at least when releasing production code. Developers find that user experience is best when a single medium-sized bundle of code is loaded when a user first visits a website than when many small modules are loaded.
...@@ -257,7 +257,7 @@ In a language like JavaScript that does not require compilation, running a bundl ...@@ -257,7 +257,7 @@ In a language like JavaScript that does not require compilation, running a bundl
Some bundlers also support a “hot module replacement” mode for developers where each time a bundle is regenerated, it is automatically loaded into the browser. When this works, it is a magical experience for developers, but there are some tricks going on under the hood to make it work, and it is not suitable for all projects. Some bundlers also support a “hot module replacement” mode for developers where each time a bundle is regenerated, it is automatically loaded into the browser. When this works, it is a magical experience for developers, but there are some tricks going on under the hood to make it work, and it is not suitable for all projects.
17.6 Transpilation with Babel ## 17.6 Transpilation with Babel
Babel is a tool that compiles JavaScript written using modern language features into JavaScript that does not use those modern language features. Because it compiles JavaScript to JavaScript, Babel is sometimes called a “transpiler.” Babel was created so that web developers could use the new language features of ES6 and later while still targeting web browsers that only supported ES5. Babel is a tool that compiles JavaScript written using modern language features into JavaScript that does not use those modern language features. Because it compiles JavaScript to JavaScript, Babel is sometimes called a “transpiler.” Babel was created so that web developers could use the new language features of ES6 and later while still targeting web browsers that only supported ES5.
Language features such as the ** exponentiation operator and arrow functions can be transformed relatively easily into Math.pow() and function expressions. Other language features, such as the class keyword, require much more complex transformations, and, in general, the code output by Babel is not meant to be human readable. Like bundler tools, however, Babel can produce source maps that map transformed code locations back to their original source locations, and this helps dramatically when working with transformed code. Language features such as the ** exponentiation operator and arrow functions can be transformed relatively easily into Math.pow() and function expressions. Other language features, such as the class keyword, require much more complex transformations, and, in general, the code output by Babel is not meant to be human readable. Like bundler tools, however, Babel can produce source maps that map transformed code locations back to their original source locations, and this helps dramatically when working with transformed code.
...@@ -270,7 +270,7 @@ If you use Babel and a code-bundling tool, you may be able to set up the code bu ...@@ -270,7 +270,7 @@ If you use Babel and a code-bundling tool, you may be able to set up the code bu
Even though there is less need to transform the core JavaScript language today, Babel is still commonly used to support nonstandard extensions to the language, and we’ll describe two of these language extensions in the sections that follow. Even though there is less need to transform the core JavaScript language today, Babel is still commonly used to support nonstandard extensions to the language, and we’ll describe two of these language extensions in the sections that follow.
17.7 JSX: Markup Expressions in JavaScript ## 17.7 JSX: Markup Expressions in JavaScript
JSX is an extension to core JavaScript that uses HTML-style syntax to define a tree of elements. JSX is most closely associated with the React framework for user interfaces on the web. In React, the trees of elements defined with JSX are ultimately rendered into a web browser as HTML. Even if you have no plans to use React yourself, its popularity means that you are likely to see code that uses JSX. This section explains what you need to know to make sense of of it. (This section is about the JSX language extension, not about React, and it explains only enough of React to provide context for the JSX syntax.) JSX is an extension to core JavaScript that uses HTML-style syntax to define a tree of elements. JSX is most closely associated with the React framework for user interfaces on the web. In React, the trees of elements defined with JSX are ultimately rendered into a web browser as HTML. Even if you have no plans to use React yourself, its popularity means that you are likely to see code that uses JSX. This section explains what you need to know to make sense of of it. (This section is about the JSX language extension, not about React, and it explains only enough of React to provide context for the JSX syntax.)
You can think of a JSX element as a new type of JavaScript expression syntax. JavaScript string literals are delimited with quotation marks, and regular expression literals are delimited with slashes. In the same way, JSX expression literals are delimited with angle brackets. Here is a very simple one: You can think of a JSX element as a new type of JavaScript expression syntax. JavaScript string literals are delimited with quotation marks, and regular expression literals are delimited with slashes. In the same way, JSX expression literals are delimited with angle brackets. Here is a very simple one:
...@@ -393,7 +393,7 @@ let sidebar = React.createElement(Sidebar, { ...@@ -393,7 +393,7 @@ let sidebar = React.createElement(Sidebar, {
}); });
It is a simple JSX expression, but when React renders it, it will pass the second argument (the Props object) to the first argument (the Sidebar() function) and will use the JSX expression returned by that function in place of the <Sidebar> expression. It is a simple JSX expression, but when React renders it, it will pass the second argument (the Props object) to the first argument (the Sidebar() function) and will use the JSX expression returned by that function in place of the <Sidebar> expression.
17.8 Type Checking with Flow ## 17.8 Type Checking with Flow
Flow is a language extension that allows you to annotate your JavaScript code with type information, and a tool for checking your JavaScript code (both annotated and unannotated) for type errors. To use Flow, you start writing code using the Flow language extension to add type annotations. Then you run the Flow tool to analyze your code and report type errors. Once you have fixed the errors and are ready to run the code, you use Babel (perhaps automatically as part of the code-bundling process) to strip the Flow type annotations out of your code. (One of the nice things about the Flow language extension is that there isn’t any new syntax that Flow has to compile or transform. You use the Flow language extension to add annotations to the code, and all Babel has to do is to strip those annotations out to return your code to standard JavaScript.) Flow is a language extension that allows you to annotate your JavaScript code with type information, and a tool for checking your JavaScript code (both annotated and unannotated) for type errors. To use Flow, you start writing code using the Flow language extension to add type annotations. Then you run the Flow tool to analyze your code and report type errors. Once you have fixed the errors and are ready to run the code, you use Babel (perhaps automatically as part of the code-bundling process) to strip the Flow type annotations out of your code. (One of the nice things about the Flow language extension is that there isn’t any new syntax that Flow has to compile or transform. You use the Flow language extension to add annotations to the code, and all Babel has to do is to strip those annotations out to return your code to standard JavaScript.)
TYPESCRIPT VERSUS FLOW TYPESCRIPT VERSUS FLOW
...@@ -411,7 +411,7 @@ When I first started using Flow, I found that it was sometimes difficult to unde ...@@ -411,7 +411,7 @@ When I first started using Flow, I found that it was sometimes difficult to unde
This section is a tutorial, and it does not attempt to cover Flow comprehensively. If you decide to try Flow, you will almost certainly end up spending time reading the documentation at https://flow.org. On the other hand, you do not need to master the Flow type system before you can start making practical use of it in your projects: the simple uses of Flow described here will take you a long way. This section is a tutorial, and it does not attempt to cover Flow comprehensively. If you decide to try Flow, you will almost certainly end up spending time reading the documentation at https://flow.org. On the other hand, you do not need to master the Flow type system before you can start making practical use of it in your projects: the simple uses of Flow described here will take you a long way.
17.8.1 Installing and Running Flow ### 17.8.1 Installing and Running Flow
Like the other tools described in this chapter, you can install the Flow type-checking tool using a package manager, with a command like npm install -g flow-bin or npm install --save-dev flow-bin. If you install the tool globally with -g, then you can run it with flow. And if you install it locally in your project with --save-dev, then you can run it with npx flow. Before using Flow to do type checking, the first time run it as flow --init in the root directory of your project to create a .flowconfig configuration file. You may never need to add anything to this file, but Flow needs it to know where your project root is. Like the other tools described in this chapter, you can install the Flow type-checking tool using a package manager, with a command like npm install -g flow-bin or npm install --save-dev flow-bin. If you install the tool globally with -g, then you can run it with flow. And if you install it locally in your project with --save-dev, then you can run it with npx flow. Before using Flow to do type checking, the first time run it as flow --init in the root directory of your project to create a .flowconfig configuration file. You may never need to add anything to this file, but Flow needs it to know where your project root is.
When you run Flow, it will find all the JavaScript source code in your project, but it will only report type errors for the files that have “opted in” to type checking by adding a // @flow comment at the top of the file. This opt-in behavior is important because it means that you can adopt Flow for existing projects and then begin to convert your code one file at a time, without being bothered by errors and warnings on files that have not yet been converted. When you run Flow, it will find all the JavaScript source code in your project, but it will only report type errors for the files that have “opted in” to type checking by adding a // @flow comment at the top of the file. This opt-in behavior is important because it means that you can adopt Flow for existing projects and then begin to convert your code one file at a time, without being bothered by errors and warnings on files that have not yet been converted.
...@@ -445,7 +445,7 @@ Cannot get x.length because property length is missing in Number [1]. ...@@ -445,7 +445,7 @@ Cannot get x.length because property length is missing in Number [1].
[1] 5│ let s = size(1000); [1] 5│ let s = size(1000);
Flow sees that the size() function takes a single argument. It doesn’t know the type of that argument, but it can see that the argument is expected to have a length property. When it sees this size() function being called with a numeric argument, it correctly flags this as an error because numbers do not have length properties. Flow sees that the size() function takes a single argument. It doesn’t know the type of that argument, but it can see that the argument is expected to have a length property. When it sees this size() function being called with a numeric argument, it correctly flags this as an error because numbers do not have length properties.
17.8.2 Using Type Annotations ### 17.8.2 Using Type Annotations
When you declare a JavaScript variable, you can add a Flow type annotation to it by following the variable name with a colon and the type: When you declare a JavaScript variable, you can add a Flow type annotation to it by following the variable name with a colon and the type:
let message: string = "Hello world"; let message: string = "Hello world";
...@@ -511,7 +511,7 @@ Flow syntax allows a question mark before any type specification to indicate tha ...@@ -511,7 +511,7 @@ Flow syntax allows a question mark before any type specification to indicate tha
So far, we’ve discussed primitive types string, number, boolean, null, and void and have demonstrated how you can use use them with variable declarations, function parameters, and function return values. The subsections that follow describe some more complex types supported by Flow. So far, we’ve discussed primitive types string, number, boolean, null, and void and have demonstrated how you can use use them with variable declarations, function parameters, and function return values. The subsections that follow describe some more complex types supported by Flow.
17.8.3 Class Types ### 17.8.3 Class Types
In addition to the primitive types that Flow knows about, it also knows about all of JavaScript’s built-in classes and allows you to use class name as types. The following function, for example, uses type annotations to indicate that it should be invoked with one Date object and one RegExp object: In addition to the primitive types that Flow knows about, it also knows about all of JavaScript’s built-in classes and allows you to use class name as types. The following function, for example, uses type annotations to indicate that it should be invoked with one Date object and one RegExp object:
// @flow // @flow
...@@ -546,7 +546,7 @@ export default class Complex { ...@@ -546,7 +546,7 @@ export default class Complex {
// This assignment would not be allowed by Flow if there was not a // This assignment would not be allowed by Flow if there was not a
// type annotation for i inside the class. // type annotation for i inside the class.
Complex.i = new Complex(0,1); Complex.i = new Complex(0,1);
17.8.4 Object Types ### 17.8.4 Object Types
The Flow type to describe an object looks a lot like an object literal, except that property values are replaced by property types. Here, for example, is a function that expects an object with numeric x and y properties: The Flow type to describe an object looks a lot like an object literal, except that property values are replaced by property types. Here, for example, is a function that expects an object with numeric x and y properties:
// @flow // @flow
...@@ -573,7 +573,7 @@ const cityLocations : {[string]: {longitude:number, latitude:number}} = { ...@@ -573,7 +573,7 @@ const cityLocations : {[string]: {longitude:number, latitude:number}} = {
// TODO: if there are any other important cities, add them here. // TODO: if there are any other important cities, add them here.
}; };
export default cityLocations; export default cityLocations;
17.8.5 Type Aliases ### 17.8.5 Type Aliases
Objects can have many properties, and the Flow type that describes such an object will be long and difficult to type. And even relatively short object types can be confusing because they look so much like object literals. Once we get beyond simple types like number and ?string, it is often useful to be able to define names for our Flow types. And in fact, Flow uses the type keyword to do exactly that. Follow the type keyword with an identifier, an equals sign, and a Flow type. Once you’ve done that, the identifier will be an alias for the type. Here, for example, is how we could rewrite the distance() function from the previous section with an explicitly defined Point type: Objects can have many properties, and the Flow type that describes such an object will be long and difficult to type. And even relatively short object types can be confusing because they look so much like object literals. Once we get beyond simple types like number and ?string, it is often useful to be able to define names for our Flow types. And in fact, Flow uses the type keyword to do exactly that. Follow the type keyword with an identifier, an equals sign, and a Flow type. Once you’ve done that, the identifier will be an alias for the type. Here, for example, is how we could rewrite the distance() function from the previous section with an explicitly defined Point type:
// @flow // @flow
...@@ -590,7 +590,7 @@ Note that this code exports the distance() function and also exports the Point t ...@@ -590,7 +590,7 @@ Note that this code exports the distance() function and also exports the Point t
Finally, it is worth noting that instead of defining a name for a Flow object type that represents a point, it would probably be simpler and cleaner to just define a Point class and use that class as the type. Finally, it is worth noting that instead of defining a name for a Flow object type that represents a point, it would probably be simpler and cleaner to just define a Point class and use that class as the type.
17.8.6 Array Types ### 17.8.6 Array Types
The Flow type to describe an array is a compound type that also includes the type of the array elements. Here, for example, is a function that expects an array of numbers, and the error that Flow reports if you try to call the function with an array that has non-numeric elements: The Flow type to describe an array is a compound type that also includes the type of the array elements. Here, for example, is a function that expects an array of numbers, and the error that Flow reports if you try to call the function with an array that has non-numeric elements:
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ average.js:8:16 Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ average.js:8:16
...@@ -640,7 +640,7 @@ function size(s: Array<mixed>): number { ...@@ -640,7 +640,7 @@ function size(s: Array<mixed>): number {
console.log(size([1,true,"three"])); console.log(size([1,true,"three"]));
The element type mixed indicates that the elements of the array can be of any type. If our function actually indexed the array and attempted to use any of those elements, Flow would insist that we use typeof checks or other tests to determine the type of the element before performing any unsafe operation on it. (If you are willing to give up on type checking, you can also use any instead of mixed: it allows you to do whatever you want with the values of the array without ensuring that the values are of the type you expect.) The element type mixed indicates that the elements of the array can be of any type. If our function actually indexed the array and attempted to use any of those elements, Flow would insist that we use typeof checks or other tests to determine the type of the element before performing any unsafe operation on it. (If you are willing to give up on type checking, you can also use any instead of mixed: it allows you to do whatever you want with the values of the array without ensuring that the values are of the type you expect.)
17.8.7 Other Parameterized Types ### 17.8.7 Other Parameterized Types
We’ve seen that when you annotate a value as an Array, Flow requires you to also specify the type of the array elements inside angle brackets. This additional type is known as a type parameter, and Array is not the only JavaScript class that is parameterized. We’ve seen that when you annotate a value as an Array, Flow requires you to also specify the type of the array elements inside angle brackets. This additional type is known as a type parameter, and Array is not the only JavaScript class that is parameterized.
JavaScript’s Set class is a collection of elements, like an array is, and you can’t use Set as a type by itself, but you have to include a type parameter within angle brackets to specify the type of the values contained in the set. (Though you can use mixed or any if the set may contain values of multiple types.) Here’s an example: JavaScript’s Set class is a collection of elements, like an array is, and you can’t use Set as a type by itself, but you have to include a type parameter within angle brackets to specify the type of the values contained in the set. (Though you can use mixed or any if the set may contain values of multiple types.) Here’s an example:
...@@ -710,7 +710,7 @@ function zip<A,B>(a:Array<A>, b:Array<B>): Array<[?A,?B]> { ...@@ -710,7 +710,7 @@ function zip<A,B>(a:Array<A>, b:Array<B>): Array<[?A,?B]> {
// Create the array [[1,'a'], [2,'b'], [3,'c'], [4,undefined]] // Create the array [[1,'a'], [2,'b'], [3,'c'], [4,undefined]]
let pairs: Array<[?number,?string]> = zip([1,2,3,4], ['a','b','c']) let pairs: Array<[?number,?string]> = zip([1,2,3,4], ['a','b','c'])
17.8.8 Read-Only Types ### 17.8.8 Read-Only Types
Flow defines some special parameterized “utility types” that have names beginning with $. Most of these types have advanced use cases that we are not going to cover here. But two of them are quite useful in practice. If you have an object type T and want to make a read-only version of that type, just write $ReadOnly<T>. Similarly, you can write $ReadOnlyArray<T> to describe a read-only array with elements of type T. Flow defines some special parameterized “utility types” that have names beginning with $. Most of these types have advanced use cases that we are not going to cover here. But two of them are quite useful in practice. If you have an object type T and want to make a read-only version of that type, just write $ReadOnly<T>. Similarly, you can write $ReadOnlyArray<T> to describe a read-only array with elements of type T.
The reason to use these types is not because they can offer any guarantee that an object or array can’t be modified (see Object.freeze() in §14.2 if you want true read-only objects) but because it allows you to catch bugs caused by unintentional modifications. If you write a function that takes an object or array argument and does not change any of the object’s properties or the array’s elements, then you can annotate the function parameter with one of Flow’s read-only types. If you do this, then Flow will report an error if you forget and accidentally modify the input value. Here are two examples: The reason to use these types is not because they can offer any guarantee that an object or array can’t be modified (see Object.freeze() in §14.2 if you want true read-only objects) but because it allows you to catch bugs caused by unintentional modifications. If you write a function that takes an object or array argument and does not change any of the object’s properties or the array’s elements, then you can annotate the function parameter with one of Flow’s read-only types. If you do this, then Flow will report an error if you forget and accidentally modify the input value. Here are two examples:
...@@ -735,7 +735,7 @@ function average(data: $ReadOnlyArray<number>): number { ...@@ -735,7 +735,7 @@ function average(data: $ReadOnlyArray<number>): number {
let data: Array<number> = [1,2,3,4,5]; let data: Array<number> = [1,2,3,4,5];
average(data) // => 3 average(data) // => 3
17.8.9 Function Types ### 17.8.9 Function Types
We have seen how to add type annotations to specify the types of a function’s parameters and its return type. But when one of the parameters of a function is itself a function, we need to be able to specify the type of that function parameter. We have seen how to add type annotations to specify the types of a function’s parameters and its return type. But when one of the parameters of a function is itself a function, we need to be able to specify the type of that function parameter.
To express the type of a function with Flow, write the types of each parameter, separate them with commas, enclose them in parentheses, and then follow that with an arrow and type return type of the function. To express the type of a function with Flow, write the types of each parameter, separate them with commas, enclose them in parentheses, and then follow that with an arrow and type return type of the function.
...@@ -760,7 +760,7 @@ export default function fetchText(url: string, callback: FetchTextCallback) { ...@@ -760,7 +760,7 @@ export default function fetchText(url: string, callback: FetchTextCallback) {
callback(error, status, null); callback(error, status, null);
}); });
} }
17.8.10 Union Types ### 17.8.10 Union Types
Let’s return one more time to the size() function. It doesn’t really make sense to have a function that does nothing other than return the length of an array. Arrays have a perfectly good length property for that. But size() might be useful if it could take any kind of collection object (an array or a Set or a Map) and return the number of elements in the collection. In regular untyped JavaScript it would be easy to write a size() function like that. With Flow, we need a way to express a type that allows arrays, Sets, and Maps, but doesn’t allow values of any other type. Let’s return one more time to the size() function. It doesn’t really make sense to have a function that does nothing other than return the length of an array. Arrays have a perfectly good length property for that. But size() might be useful if it could take any kind of collection object (an array or a Set or a Map) and return the number of elements in the collection. In regular untyped JavaScript it would be easy to write a size() function like that. With Flow, we need a way to express a type that allows arrays, Sets, and Maps, but doesn’t allow values of any other type.
Flow calls types like this Union types and allows you to express them by simply listing the desired types and separating them with vertical bar characters: Flow calls types like this Union types and allows you to express them by simply listing the desired types and separating them with vertical bar characters:
...@@ -780,7 +780,7 @@ We saw earlier that putting a question mark before a type allows null and undefi ...@@ -780,7 +780,7 @@ We saw earlier that putting a question mark before a type allows null and undefi
In general, when you annotate a value with a Union type, Flow will not allow you to use that value until you’ve done enough tests to figure out what the type of the actual value is. In the size() example we just looked at, we need to explicitly check whether the argument is an array before we try to access the length property of the argument. Note that we do not have to distinguish a Set argument from a Map argument, however: both of those classes define a size property, so the code in the else clause is safe as long as the argument is not an array. In general, when you annotate a value with a Union type, Flow will not allow you to use that value until you’ve done enough tests to figure out what the type of the actual value is. In the size() example we just looked at, we need to explicitly check whether the argument is an array before we try to access the length property of the argument. Note that we do not have to distinguish a Set argument from a Map argument, however: both of those classes define a size property, so the code in the else clause is safe as long as the argument is not an array.
17.8.11 Enumerated Types and Discriminated Unions ### 17.8.11 Enumerated Types and Discriminated Unions
Flow allows you to use primitive literals as types that consist of that one single value. If you write let x:3;, then Flow will not allow you to assign any value to that variable other than 3. It is not often useful to define types that have only a single member, but a union of literal types can be useful. You can probably imagine a use for types like these, for example: Flow allows you to use primitive literals as types that consist of that one single value. If you write let x:3;, then Flow will not allow you to assign any value to that variable other than 3. It is not often useful to define types that have only a single member, but a union of literal types can be useful. You can probably imagine a use for types like these, for example:
type Answer = "yes" | "no"; type Answer = "yes" | "no";
...@@ -851,7 +851,7 @@ function handleMessageFromReticulator(message: WorkerMessage) { ...@@ -851,7 +851,7 @@ function handleMessageFromReticulator(message: WorkerMessage) {
console.info(message.splinesPerSecond); console.info(message.splinesPerSecond);
} }
} }
17.9 Summary ## 17.9 Summary
JavaScript is the most-used programming language in the world today. It is a living language—one that continues to evolve and improve—surrounded by a flourishing ecosystem of libraries, tools, and extensions. This chapter introduced some of those tools and extensions, but there are many more to learn about. The JavaScript ecosystem flourishes because the JavaScript developer community is active and vibrant, full of peers who share their knowledge through blog posts, videos, and conference presentations. As you close this book and go forth to join this community, you will find no shortage of information sources to keep you engaged with and learning about JavaScript. JavaScript is the most-used programming language in the world today. It is a living language—one that continues to evolve and improve—surrounded by a flourishing ecosystem of libraries, tools, and extensions. This chapter introduced some of those tools and extensions, but there are many more to learn about. The JavaScript ecosystem flourishes because the JavaScript developer community is active and vibrant, full of peers who share their knowledge through blog posts, videos, and conference presentations. As you close this book and go forth to join this community, you will find no shortage of information sources to keep you engaged with and learning about JavaScript.
Best wishes, David Flanagan, March 2020 Best wishes, David Flanagan, March 2020
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册