diff --git a/chapters/ch01.asciidoc b/chapters/ch01.asciidoc index a8f7bf9..5472201 100644 --- a/chapters/ch01.asciidoc +++ b/chapters/ch01.asciidoc @@ -1,7 +1,7 @@ [[ecmascript-and-the-future-of-javascript]] == ECMAScript and the Future of JavaScript -JavaScript has gone from being a 1995 marketing ploy to gain a tactical advantage, to becoming the core programming experience in the world's most widely used application runtime platform in 2017. The language doesn't merely run in browsers anymore, but is also used to create desktop and mobile applications, in hardware devices, and even in the vacuum of space. +JavaScript has gone from being a 1995 marketing ploy to gain a tactical advantage, to becoming the core programming experience in the world's most widely used application runtime platform in 2017. The language doesn't merely run in browsers anymore, but is also used to create desktop and mobile applications, in hardware devices, and even in space suit design at NASA. How did JavaScript get here, and where is it going next? @@ -47,7 +47,7 @@ Having spent ten years without observing significant change to the language spec Since ES6 came out, TC39 has streamlinedfootnote:[You can find the September 2013 presentation which lead to the streamlined proposal revisioning process here: https://mjavascript.com/out/tc39-improvement.] its proposal revisioning process and adjusted it to meet modern expectations: the need to iterate more often and consistently, and to democratize specification development. At this point, TC39 moved from an ancient Word-based flow to using ecmarkup and GitHub Pull Requests, greatly increasing the number of proposalsfootnoteref:[proposals,You can find all proposals being considered by TC39 at https://mjavascript.com/out/tc39-proposals.] being created as well as external participation by non-members. -Firefox, Chrome, Edge, Safari and Node.js all offer over 95% compliancy of the ES6 specification,footnote:[For a detailed ES6 compatibility report across browsers, check out the following table: https://mjavascript.com/out/es6-compat.] but we’ve been able to use the features as they came out in each of these browsers rather than having to wait until the flip of a switch when their implementation of ES6 was 100% finalized. +Firefox, Chrome, Edge, Safari and Node.js all offer over 95% compliance of the ES6 specification,footnote:[For a detailed ES6 compatibility report across browsers, check out the following table: https://mjavascript.com/out/es6-compat.] but we’ve been able to use the features as they came out in each of these browsers rather than having to wait until the flip of a switch when their implementation of ES6 was 100% finalized. The new process involves four different maturity stagesfootnote:[The TC39 proposal process documentation can be found at https://mjavascript.com/out/tc39-process.]. The more mature a proposal is, the more likely it is to eventually make it into the specification. @@ -160,7 +160,9 @@ For the next step, we'll replace the value of the +scripts+ property in +package [source,json] ---- { - "build": "babel src --out-dir dist" + "scripts": { + "build": "babel src --out-dir dist" + } } ---- @@ -258,12 +260,16 @@ Referencing the +node_modules/.bin+ directory, an implementation detail of how n [source,json] ---- -"lint": "eslint ." +{ + "scripts": { + "lint": "eslint ." + } +} ---- As you might recall from the Babel example, +npm+ add +node_modules+ to the +PATH+ when executing scripts. To lint our codebase, we can execute +npm run lint+ and npm will find the ESLint CLI embedded deep in the +node_modules+ directory. -Let's consider the following +example.js+ file, which is purposely ridden with style issues, to demonstrate what ESLint does. +Let's consider the following +example.js+ file, which is purposely riddled with style issues, to demonstrate what ESLint does. [source,javascript] ---- @@ -283,7 +289,11 @@ ESLint is able to fix most style problems automatically if we pass in a +--fix+ [source,json] ---- -"lint-fix": "eslint . --fix" +{ + "scripts": { + "lint-fix": "eslint . --fix" + } +} ---- When we run +lint-fix+ we'll only get a pair of errors: +hello+ is never used and +false+ is a constant condition. Every other error has been fixed in place, resulting in the bit of source code found below. The remaining errors weren't fixed because ESLint avoids making assumptions about our code, and prefers not to incur in semantic changes. In doing so, +--fix+ becomes a useful tool to resolve code style wrinkles without risking a broken program as a result. @@ -317,7 +327,7 @@ We get several new mechanics to describe asynchronous code flows in ES6: promise There's a common practice in JavaScript where developers use plain objects to create hash maps with arbitrary string keys. This can lead to vulnerabilities if we're not careful and let user input end up defining those keys. ES6 introduces a few different native built-ins to manage sets and maps, which don't have the limitation of using string keys exclusively. These collections are explored in chapter 5. -Proxy objects redefine what can be done through JavaScript reflection. Proxy objects are similar to proxies in other contexts, such as web traffic routing. They can intercept any interaction with a JavaScript object such as defining, deleting, or accessing a property. Given the mechanics of how proxies work, they are impossible to implement holistically as a polyfill. We'll devote chapter 6 to understanding proxies. +Proxy objects redefine what can be done through JavaScript reflection. Proxy objects are similar to proxies in other contexts, such as web traffic routing. They can intercept any interaction with a JavaScript object such as defining, deleting, or accessing a property. Given the mechanics of how proxies work, they are impossible to polyfill holistically: polyfills exist, but they have limitations making them incompatible with the specification in some use cases. We'll devote chapter 6 to understanding proxies. Besides new built-ins, ES6 comes with several updates to +Number+, +Math+, +Array+, and strings. In chapter 7 we'll go over a plethora of new instance and static methods added to these built-ins. diff --git a/chapters/ch02.asciidoc b/chapters/ch02.asciidoc index a6efa77..834bea0 100644 --- a/chapters/ch02.asciidoc +++ b/chapters/ch02.asciidoc @@ -232,7 +232,7 @@ var example = (parameters) => { } ---- -While arrow functions look very similar to your typical anonymous function, they are fundamentally different: arrow functions can't have a name, they can't be used as constructors, they don't have a +prototype+ property, and they are bound to their lexical scope. +While arrow functions look very similar to your typical anonymous function, they are fundamentally different: arrow functions can't be named explicitly, although modern runtimes can infer a name based on the variable they're assigned to; they can't be used as constructors nor do they have a +prototype+ property, meaning you can't use +new+ on an arrow function; and they are bound to their lexical scope, which is the reason why they don't alter the meaning of +this+. Let's dig into their semantic differences with traditional functions, the many ways to declare an arrow function, and practical use cases. @@ -261,6 +261,32 @@ If we had defined the function passed to +setInterval+ as a regular anonymous fu In a similar fashion, lexical binding in ES6 arrow functions also means that function calls won't be able to change the +this+ context when using +.call+, +.apply+, +.bind+, etc. That limitation is usually more useful than not, as it ensures that the context will always be preserved and constant. +Let's now shift our attention to the following example. What do you think the `console.log` statement will print? + +[source,javascript] +---- +function puzzle () { + return function () { + console.log(arguments) + } +} +puzzle('a', 'b', 'c')(1, 2, 3) +---- + +The answer is that `arguments` refers to the context of the anonymous function, and thus the arguments passed to that function will be printed. In this case, those arguments are `1, 2, 3`. + +What about in the following case, where we use an arrow function instead of the anonymous function in the previous example? + +[source,javascript] +---- +function puzzle () { + return () => console.log(arguments) +} +puzzle('a', 'b', 'c')(1, 2, 3) +---- + +In this case, the `arguments` object refers to the context of the `puzzle` function, because arrow functions don't create a closure. For this reason, the printed arguments will be `'a', 'b', 'c'`. + I've mentioned there's several flavors of arrow functions, but so far we've only looked at their fully fleshed version. What are the others way to represent an arrow function? ==== 2.2.2 Arrow Function Flavors @@ -274,7 +300,7 @@ var example = (parameters) => { } ---- -An arrow function with exactly one parameter can omit the parenthesis. This is optional. It's useful when passing the arrow function to another method, as it reduces the amount of parenthesis involved, making it easier for humans to parse the code. +An arrow function with exactly one parameter can omit the parenthesis. This is optional. It's useful when passing the arrow function to another method, as it reduces the amount of parenthesis involved, making it easier for some humans to parse the code. [source,javascript] ---- @@ -738,7 +764,7 @@ Let's turn our attention to spread and rest operators next. === 2.4 Rest Parameters and Spread Operator -Before ES6, interacting with an arbitrary amount of function parameters was complicated. You had to use +arguments+, which isn't an array but has a +length+ property. Usually you'd end up casting the +arguments+ object into an actual array using +Array.prototype.slice.call+, and going from there, as shown in the following snippet. +Before ES6, interacting with an arbitrary amount of function parameters was complicated. You had to use +arguments+, which isn't an array but has a +length+ property. Usually you'd end up casting the +arguments+ object into an actual array using +Array#slice.call+, and going from there, as shown in the following snippet. [source,javascript] ---- @@ -861,8 +887,8 @@ In ES6, you can combine spread with array destructuring. The following piece of [source,javascript] ---- -var [first, second, ...rest] = ['a', 'b', 'c', 'd', 'e'] -console.log(rest) +var [first, second, ...other] = ['a', 'b', 'c', 'd', 'e'] +console.log(other) // <- ['c', 'd', 'e'] ---- @@ -921,8 +947,8 @@ The following table summarizes the use cases we've discussed for the spread oper |======= |Use Case|ES5|ES6 |Concatenation|+[1, 2].concat(more)+|+[1, 2, ...more]+ -|Push onto list|+list.push.apply(list, [3, 4])+|+list.push(...[3, 4])+ -|Destructuring|+a = list[0], rest = list.slice(1)+ | +[a, ...rest] = list+ +|Push an array onto list|+list.push.apply(list, items)+|+list.push(...items)+ +|Destructuring|+a = list[0], other = list.slice(1)+ | +[a, ...other] = list+ |+new+ and +apply+|+new (Date.bind.apply(Date, [null,2015,31,8]))+| +new Date(...[2015,31,8])+ |======= @@ -1062,7 +1088,37 @@ The template we've just prepared would produce output like what's shown in the f ---- -Sometimes, it might be a good idea to pre-process the results of expressions before inserting them into your templates. For these advanced kinds of use cases, it's possible to use another feature of template literals called tagged templates. +A downside when it comes to multi-line template literals is indentation. The following example shows a typically indented piece of code with a template literal contained in a function. While we may have expected no indentation, the string is has four spaces of indentation. + +[source,javascript] +---- +function getParagraph () { + return ` + Dear Rod, + + This is a template literal string that's indented + four spaces. However, you may have expected for it + to be not indented at all. + + Nico + ` +} +---- + +While not ideal, we could get away with a utility function to remove indentation from each line in the resulting string. + +[source,javascript] +---- +function unindent (text) { + return text + .split('\n') + .map(line => line.slice(4)) + .join('\n') + .trim() +} +---- + +Sometimes, it might be a good idea to pre-process the results of interpolated expressions before inserting them into your templates. For these advanced kinds of use cases, it's possible to use another feature of template literals called tagged templates. ==== 2.5.3 Tagged Templates @@ -1126,7 +1182,7 @@ console.log(text) // <- 'Hello MAURICE, I am THRILLED to meet you!' ---- -A decidedly more useful use case would be to sanitize expressions interpolated into your templates, automatically, using a tagged template. Given a template where all expressions are considered user-input, we could use a hypothetical +sanitize+ library to remove HTML tags and similar hazards. +A decidedly more useful use case would be to sanitize expressions interpolated into your templates, automatically, using a tagged template. Given a template where all expressions are considered user-input, we could use a hypothetical +sanitize+ library to remove HTML tags and similar hazards, preventing cross site scripting (XSS) attacks where users might inject malicious HTML into our websites. [source,javascript] ---- @@ -1225,6 +1281,35 @@ console.log(i) // <- i is not defined ---- +Given +let+ variables declared in a loop are scoped to each step in the loop, the bindings would work as expected in combination with an asynchronous function call, as opposed to what we're used to with +var+. Let's look at concrete examples. + +First, we'll look at the typical example of how +var+ scoping works. The +i+ binding is scoped to the +printNumbers+ function, and its value increases all the way to +10+ as each timeout callback is scheduled. By the time each callbacks run -- one every 100 milliseconds -- +i+ has a value of +10+ and thus that's what's printed every single time. + +[source,javascript] +---- +function printNumbers () { + for (var i = 0; i < 10; i++) { + setTimeout(function () { + console.log(i) + }, i * 100) + } +} +printNumbers() +---- + +Using +let+, in contrast, binds the variable to the block's scope. Indeed, each step in the loop still increases the value of the variable, but a new binding is created each step of the way, meaning that each timeout callback will hold a reference to the binding holding the value of +i+ at the point when the callback was scheduled, printing every number from +0+ through +9+ as expected. + +[source,javascript] +---- +function printNumbers () { + for (let i = 0; i < 10; i++) { + setTimeout(function () { + console.log(i) + }, i * 100) + } +} +printNumbers() +---- One more thing of note about +let+ is a concept called the "Temporal Dead Zone". diff --git a/chapters/ch03.asciidoc b/chapters/ch03.asciidoc index acf219f..e856dca 100644 --- a/chapters/ch03.asciidoc +++ b/chapters/ch03.asciidoc @@ -1,7 +1,7 @@ [[classes-symbols-and-symbols]] == Classes, Symbols, and Objects -Now that we've covered the basic improvements to the syntax, we're in good shape to take aim at a few other additions to the language: classes, and symbols. Classes provide syntax to represent prototypal inheritance under the traditional class-based programming paradigm. Symbols are a new primitive value type in JavaScript, like strings, booleans, and numbers. They can be used for defining protocols, and in this chapter we'll investigate what that means. When we're done with classes and symbols, we'll discuss a few new static methods added to the +Object+ built-in in ES6. +Now that we've covered the basic improvements to the syntax, we're in good shape to take aim at a few other additions to the language: classes, and symbols. Classes provide syntax to represent prototypal inheritance under the traditional class-based programming paradigm. Symbols are a new primitive value type in JavaScript, like strings, booleans, and numbers. They can be used for defining protocols, and in this chapter we'll investigate what that means. When we're done with classes and symbols, we'll discuss a few new static methods added to the `Object` built-in in ES6. === 3.1 Classes @@ -221,9 +221,24 @@ console.log(person.satiety) // <- 40 ---- -Sometimes it's also important to have static methods at the class level, rather than at the instance level. JavaScript classes allow you to define such methods using the +static+ keyword, much like you would use +get+ or +set+ as a prefix to a method definition that's a getter or a setter. +Sometimes it's necessary to add static methods at the class level, rather than members at the instance level. Using syntax available before ES6, instance members have to be explicitly added to the prototype chain. Meanwhile, static methods should be added to the constructor directly. -The following example defines a +MathHelper+ class with a static +sum+ method that's able to calculate the sum of all numbers passed to it in a function call, by taking advantage of the +Array.prototype.reduce+ method. +[source,javascript] +---- +function Person () { + this.hunger = 100 +} +Person.prototype.eat = function () { + this.hunger-- +} +Person.isPerson = function (person) { + return person instanceof Person +} +---- + +JavaScript classes allow you to define static methods like +Person.isPerson+ using the +static+ keyword, much like you would use +get+ or +set+ as a prefix to a method definition that's a getter or a setter. + +The following example defines a +MathHelper+ class with a static +sum+ method that's able to calculate the sum of all numbers passed to it in a function call, by taking advantage of the `Array#reduce` method. [source,javascript] ---- @@ -253,7 +268,7 @@ Banana.prototype.slice = function () { } ---- -Given the ephemeral knowledge one has to remember, and the fact that `Object.create` was only made available in ES5, JavaScript developers have historically turned to libraries to resolve their prototype inheritance issues. One such example is +util.inherits+ in Node.js, which is usually favored over `Object.create` for legacy support reasons. +Given the ephemeral knowledge one has to remember, and the fact that `Object.create` was only made available in ES5, JavaScript developers have historically turned to libraries to resolve their prototype inheritance issues. One such example is `util.inherits` in Node.js, which is usually favored over `Object.create` for legacy support reasons. [source,javascript] ---- @@ -673,7 +688,7 @@ function md (input, options=defaults) { } ---- -The default values have to be merged with user-provided configuration, somehow. That's where +Object.assign+ comes in, as shown in the following example. Here, we start with an empty +{}+ object, copy our default values over to it, and then copy the options on top. The resulting +config+ object will have all of the default values plus the user-provided configuration. +The default values have to be merged with user-provided configuration, somehow. That's where +Object.assign+ comes in, as shown in the following example. We start with an empty +{}+ object, -- which will be mutated and returned by `Object.assign` -- we copy the default values over to it, and then copy the options on top. The resulting +config+ object will have all of the default values plus the user-provided configuration. [source,javascript] ---- @@ -682,6 +697,23 @@ function md (input, options) { } ---- +.Understanding the `target` of `Object.assign` +[WARNING] +==== +The `Object.assign` function mutates its first argument. It's signature is `(target, ...sources)`. Every source is applied onto the target object, source by source and property by property. + +Consider the following scenario, where we don't pass an empty object as the first argument of `Object.assign`, instead just providing it with the `defaults` and the `options`. We would be changing the contents of the `defaults` object, losing some of our default values -- and obtaining some wrong ones -- in the process of mutating the object. The first invocation would produce the same result as the previous example, but it would modify our defaults in the process, changing how subsequent calls to +md+ work. + +[source,javascript] +---- +function md (input, options) { + const config = Object.assign(defaults, options) +} +---- + +For this reason, it's generally best to pass a brand new object on the first position, every time. +==== + For any properties that had a default value where the user also provided a value, the user-provided value will prevail. Here's how +Object.assign+ works. First, it takes the first argument passed to it, let's call it +target+. It then iterates over all keys of each of the other arguments, let's call them +sources+. For each source in +sources+, all of its properties are iterated and assigned to +target+. The end result is that right-most sources -- in our case, the +options+ object -- overwrite any previously assigned values, as shown in the following bit of code. [source,javascript] @@ -739,9 +771,9 @@ Object.assign({}, { a: ['b', 'c', 'd'] }, { a: ['e', 'f'] }) // <- { a: ['e', 'f'] } ---- -At the time of this writing, there's an ECMAScript stage 3 proposal to implement spread in objects, similar to how you can spread iterable objects onto an array in ES6. Spreading an object onto another is equivalent to using an +Object.assign+ function call. +At the time of this writing, there's an ECMAScript stage 3 proposalfootnote:[You can find the proposal draft at: https://mjavascript.com/out/object-spread.] to implement spread in objects, similar to how you can spread iterable objects onto an array in ES6. Spreading an object onto another is equivalent to using an +Object.assign+ function call. -The following piece of code shows a few cases where we're spreading the properties of an object onto another one, and the +Object.assign+ counterpart. As you can see, using object spread is more succint and should be preferred where possible. +The following piece of code shows a few cases where we're spreading the properties of an object onto another one, and their +Object.assign+ counterpart. As you can see, using object spread is more succint and should be preferred where possible. [source,javascript] ---- @@ -810,7 +842,7 @@ Object.is(-0, +0) // <- false ---- -These differences may not seem like much, but dealing with +NaN+ has always been cumbersome because of its special quirks, such as +typeof NaN+ being +'number'+ and it not being equal to itself. +These differences may not seem like much, but dealing with +NaN+ has always been cumbersome because of its special quirks, such as +typeof NaN+ being `'number'` and it not being equal to itself. ==== 3.3.3 +Object.setPrototypeOf+ @@ -825,7 +857,7 @@ const cat = Object.create(baseCat) cat.name = 'Milanesita' ---- -The `Object.create` method is, however, limited to newly created objects. In contrast, we could use +Object.setPrototypeOf+ to change the prototype of an object that already exists, as shown in the following code snippet. +The `Object.create` method is, however, limited to newly created objects. In contrast, we could use `Object.setPrototypeOf` to change the prototype of an object that already exists, as shown in the following code snippet. [source,javascript] ---- @@ -833,12 +865,12 @@ const baseCat = { type: 'cat', legs: 4 } const cat = Object.setPrototypeOf({ name: 'Milanesita' }, baseCat) ---- -Note however that there are serious performance implications when using +Obect.setPrototypeOf+ as opposed to `Object.create`, and some careful consideration is in order before you decide to go ahead and sprinkle +Object.setPrototypeOf+ all over a codebase. +Note however that there are serious performance implications when using `Obect.setPrototypeOf` as opposed to `Object.create`, and some careful consideration is in order before you decide to go ahead and sprinkle `Object.setPrototypeOf` all over a codebase. .Performance issues [WARNING] ==== -Using +Object.setPrototypeOf+ to change the prototype of an object is an expensive operation. Here is what the Mozilla Developer Network documentation has to say about the matter. +Using `Object.setPrototypeOf` to change the prototype of an object is an expensive operation. Here is what the Mozilla Developer Network documentation has to say about the matter. [quote, Mozilla Developer Network] ____ diff --git a/chapters/ch04.asciidoc b/chapters/ch04.asciidoc index 2208de2..14230dc 100644 --- a/chapters/ch04.asciidoc +++ b/chapters/ch04.asciidoc @@ -7,11 +7,11 @@ To kick off the chapter, we'll start by discussing promises. Promises have exist === 4.1 Promises -Promises can be vaguely defined as "a proxy for a value that will eventually become available". While we can write synchronous code inside Promises, Promise-based code flows in a strictly asynchronous manner. Promises make asynchronous flows easier to reason about -- once you've mastered promises, that is. +Promises can be vaguely defined as "a proxy for a value that will eventually become available". While we can write synchronous code inside promises, promise-based code flows in a strictly asynchronous manner. Promises can make asynchronous flows easier to reason about -- once you've mastered promises, that is. ==== 4.1.1 Getting Started with Promises -As an example, let's take a look at the upcoming browser +fetch+ API. This API is a simplification of +XMLHttpRequest+. It aims to be super simple to use for the most basic use cases: making a +GET+ request against an HTTP resource. It also provides a comprehensive API that caters to advanced use cases as well, but that's not our focus for now. In its most basic incarnation, you can make a +GET /items+ request using a piece of code like the following. +As an example, let's take a look at the upcoming browser +fetch+ API. This API is a simplification of +XMLHttpRequest+. It aims to be super simple to use for the most basic use cases: making a +GET+ request against an HTTP resource. It provides an extensive API that caters to advanced use cases, but that's not our focus for now. In its most basic incarnation, you can make a +GET /items+ HTTP request using a piece of code like the following. [source,javascript] ---- @@ -71,7 +71,7 @@ p.then(null, error => { .Promises as an alternative to callbacks and events **** -Traditionally JavaScript relied on callbacks instead of promises and chaining. If the +fetch+ function asked for a callback, you'd have to add one that would then get executed whenever the fetch operation ends. Typical asynchronous code flow conventions in Node.js established a best practice of reserving the first parameter in the callback for errors -- that may or may not occur -- during the fetching process. The rest of the parameters can be used to read the results of the asynchronous operation. Most commonly, a single data parameter is used. The next bit of code shows how +fetch+ would look like if it had a callback-based API. +Traditionally JavaScript relied on callbacks instead of promises and chaining. If the +fetch+ function asked for a callback, you'd have to add one that would then get executed whenever the fetch operation ends. Typical asynchronous code flow conventions in Node.js established a best practice of reserving the first parameter in the callback for errors -- that may or may not occur -- during the fetching process. The rest of the parameters can be used to read the results of the asynchronous operation. Most commonly, a single data parameter is used. The next bit of code shows how +fetch+ would look if it had a callback-based API. [source,javascript] ---- @@ -86,7 +86,7 @@ fetch('/items', (err, res) => { The callback wouldn't be invoked until the +/items+ resource has been retrieved, or an error arises from the +fetch+ operation. Execution remains asynchronous and non-blocking. Note that in this model you could only specify a single callback. That callback would be responsible for all functionality derived from the response, and it'd be up to the consumer to come up with a mechanism to compose different aspects of handling the response into that single callback. -Besides traditional callbacks, another API design choice might have been to use an event-driven model. In this case the object returned by +fetch+ would be able to register callbacks for different kinds of events, binding as many event handlers as needed for any events -- just like when you attach event listeners to the browser DOM. Typically there's an +error+ event that's raised when things go awry, and other events that are raised when something notable happens. In the following piece of code, we show how +fetch+ would look like if it had an event-based API. +Besides traditional callbacks, another API design choice might have been to use an event-driven model. In this case the object returned by +fetch+ would be able to register callbacks for different kinds of events, binding as many event handlers as needed for any events -- just like when you attach event listeners to the browser DOM. Typically there's an +error+ event that's raised when things go awry, and other events that are raised when something notable happens. In the following piece of code, we show how +fetch+ would look if it had an event-based API. [source,javascript] ---- @@ -104,10 +104,18 @@ Binding several listeners for each type of event would eliminate the concern we When it comes to promises, chaining is a major source of confusion. In an event-based API, chaining is made possible by having the +.on+ method attach the event listener and then returning the event emitter itself. Promises are different. The +.then+ and +.catch+ methods return a new promise every time. That's important because chaining can have wildly different results depending on where you append a +.then+ or a +.catch+ call onto. -.A major source of confusion +.Visualizing Promise chains: a major source of confusion [WARNING] ==== The +.then+ and +.catch+ methods return a new promise every time, creating a tree-like data structure. If you had a +p1+ promise and a +p2+ promise returned by +p1.then+, the +p1+ and +p2+ promises would be nodes connected by the +p1.then+ reaction handler. Reactions create new promises that are attached to the tree as children of the promise they're reacting to. + +When we chain promises, we need to understand that `p1.then(r1).then(r2)` creates two new `p2` and `p3` promises. The second reaction, `r2`, is going to fire if `p2` fulfills, while the `r1` reaction will fire when `p1` is fulfilled. When we have a statement such as `p1.then(r1); p1.then(r2)`, in contrast, both `r1` and `r2` will fire if `p1` is fulfilled. A discrepancy occurs when `p1` fulfills but `p2` doesn't. + +Figuring out the tree-like nature of promises is the key to unlocking a deep understanding of how promises behave. To this end, I've created an online tool called Promisees you can use to play around with promise chains while visualizing the tree structure they leave behind. + +image::../images/c04g01-promisees.png["Promisees lets you write a piece of code and visualize how the underlying graph evolves as promises are settled in fulfillment or rejection."] + +You can find Promisees at https://mjavascript.com/out/promisees. ==== A promise is created by passing the +Promise+ constructor a resolver that decides how and when the promise is settled, by calling either a +resolve+ method that will settle the promise in fulfillment or a +reject+ method that'd settle the promise as a rejection. Until the promise is settled by calling either function, it'll be in pending state and any reactions attached to it won't be executed. The following snippet of code creates a promise from scratch where we'll wait for a second before randomly settling the promise with a fulfillment or rejection result. @@ -135,7 +143,7 @@ Promise // <- 123 ---- -When a +p+ promise is fulfilled, reactions registered with +p.then+ are executed. When a +p+ promise is rejected, reactions registered with +p.catch+ are executed. Those reactions can, in turn, result in three different situations depending on whether they return a value, +throw+ an error, or return a +Promise+ or thenable. Thenables are objects considered promise-like that can be casted into a +Promise+ using +Promise.resolve+ as observed in section 4.1.3. +When a +p+ promise is fulfilled, reactions registered with +p.then+ are executed. When a +p+ promise is rejected, reactions registered with +p.catch+ are executed. Those reactions can, in turn, result in three different situations depending on whether they return a value, a +Promise+, a thenable, or +throw+ an error. Thenables are objects considered promise-like that can be casted into a +Promise+ using +Promise.resolve+ as observed in section 4.1.3. A reaction may return a value, which would cause the promise returned by +.then+ to become fulfilled with that value. In this sense, promises can be chained to transform the fulfillment value of the previous promise over and over, as shown in the following snippet of code. @@ -719,7 +727,7 @@ This sort of abstraction of complexity into another function often helps keep co ==== Iterators don't have any knowledge that the sequences they produce are infinite. In a similar situation to the famous halting problem, there is no way of knowing whether the sequence is infinite or not in code. -image::../images/c04g01-halting-problem.png["The halting problem depicted by XKCD comic 1266"] +image::../images/c04g02-halting-problem.png["The halting problem depicted in an XKCD comic: https://mjavascript.com/out/xkcd-1266."] You typically have a good idea of whether a sequence is infinite or not. Whenever you have an infinite sequence it's up to you to add an escape condition that ensures the program won't crash in an attempt to loop over every single value in the sequence. While +for..of+ won't run into the problem unless there's no escape condition, using mechanisms such as spread or +Array.from+ would immediately result in the program crashing into an infinite loop in the case of infinite sequences. ==== @@ -749,7 +757,7 @@ const colors = { green: '#0e0', orange: '#f50', pink: '#e07', - [Symbol.iterator] () { + [Symbol.iterator]() { const keys = Object.keys(colors) return { next () { @@ -832,26 +840,26 @@ const songs = [ We could create a +playlist+ function that returns a sequence, representing all the songs that will be played by our application. This function would take the +songs+ provided by the human as well as the +repeat+ value, which indicates how many times they want the songs to be reproduced in a loop -- once, twice, or +Infinity+ times -- before coming to an end. -The following piece of code shows how we could implement +playlist+. We could start with an empty playlist. In each turn of the loop we'll check if there are any songs left to play. If there aren't any songs left, and we have a +repeat+ value above zero, we'll create a +copy+ of the song list provided by the user. We use that +copy+ as state, to know where we are in their song list. We'll return the first song in the list by pulling it with +.shift+, until there aren't any songs left in our +copy+. The sequence ends when there aren't any songs left and +repeat+ is zero or less. +The following piece of code shows how we could implement +playlist+. We start with an empty playlist and use an +index+ number to track where in the song list we are positioned. We return the next song in the list by incrementing the +index+, until there aren't any songs left in the current loop. At this point we decrement om the `repeat` count and reset the `index`. The sequence ends when there aren't any songs left and +repeat+ reaches zero. [source,javascript] ---- function playlist (songs, repeat) { return { - [Symbol.iterator] () { - let copy = [] + [Symbol.iterator]() { + let index = 0 return { next () { - if (copy.length === 0) { - if (repeat < 1) { - return { done: true } - } - copy = songs.slice() + if (index >= songs.length) { repeat-- + index = 0 } - return { - value: copy.shift(), done: false + if (repeat < 1) { + return { done: true } } + const song = songs[index] + index++ + return { done: false, value: song } } } } @@ -867,7 +875,7 @@ console.log([...playlist(['a', 'b'], 3)]) // <- ['a', 'b', 'a', 'b', 'a', 'b'] ---- -To iterate over the playlist we'd probably come up with a +player+ function. Assuming a +playSong+ function that reproduces a song and invokes a callback when the song ends, our +player+ implementation could look like the following function, where we asynchronously loop the iterator coming from a sequence, requesting new songs as previous ones finish playback. Given that there's always a considerable waiting period in between +g.next+ calls -- while the songs are actually played inside +playSong+ -- there's no risk of running into an infinite loop even when the sequence produced by +playlist+ is infinite. +To iterate over the playlist we'd probably come up with a +player+ function. Assuming a +playSong+ function that reproduces a song and invokes a callback when the song ends, our +player+ implementation could look like the following function, where we asynchronously loop the iterator coming from a sequence, requesting new songs as previous ones finish playback. Given that there's always a considerable waiting period in between +g.next+ calls -- while the songs are actually playing inside +playSong+ -- there's little risk of being stuck in an infinite loop that'd crash the runtime, even when the sequence produced by +playlist+ is infinite. [source,javascript] ---- @@ -898,37 +906,35 @@ const sequence = playlist(songs, Infinity) player(sequence) ---- -A change allowing the human to shuffle their playlist wouldn't be complicated to introduce. We'd have to tweak the +playlist+ function to include a +shuffle+ flag. That way, each step where we reproduce the list of user-provided songs could +A change allowing the human to shuffle their playlist wouldn't be complicated to introduce. We'd have to tweak the +playlist+ function to include a +shuffle+ flag, and if that flag is present we'd sort the song list at random. [source,javascript] ---- -function playlist (songs, repeat, shuffle) { +function playlist (inputSongs, repeat, shuffle) { + const songs = shuffle ? shuffleSongs(inputSongs) : inputSongs return { - [Symbol.iterator] () { - let copy = [] + [Symbol.iterator]() { + let index = 0 return { next () { - if (copy.length === 0) { - if (repeat < 1) { - return { done: true } - } - copy = songs.slice() + if (index >= songs.length) { repeat-- + index = 0 } - const value = shuffle ? randomSong() : nextSong() - return { done: false, value } + if (repeat < 1) { + return { done: true } + } + const song = songs[index] + index++ + return { done: false, value: song } } } - function randomSong () { - const index = Math.floor(Math.random() * copy.length) - return copy.splice(index, 1)[0] - } - function nextSong () { - return copy.shift() - } } } } +function shuffleSongs (songs) { + return songs.slice().sort(() => Math.random() > 0.5 ? 1 : -1) +} ---- Lastly, we'd have to pass in the +shuffle+ flag as +true+ if we wanted to shuffle songs in each repeat cycle. Otherwise, songs would be reproduced in the original order provided by the user. Here again we've abstracted away something that usually would involve many lines of code used to decide what song comes next into a neatly decoupled function that's only concerned with producing a sequence of songs to be reproduced by a song player. @@ -939,6 +945,17 @@ console.log([...playlist(['a', 'b'], 3, true)]) // <- ['a', 'b', 'b', 'a', 'a', 'b'] ---- +You may have noticed how the `playlist` function doesn't necessarily need to concern itself with the sort order of the songs passed to it. A better design choice may well be to extract shuffling into the calling code. If we kept the original +playlist+ code we had earlier, we could use code like the following to obtain a shuffled song collection. + +[source,javascript] +---- +function shuffleSongs (songs) { + return songs.slice().sort(() => Math.random() > 0.5 ? 1 : -1) +} +console.log([...playlist(shuffleSongs(['a', 'b']), 3)]) +// <- ['a', 'b', 'b', 'a', 'a', 'b'] +---- + Iterators are an important tool in ES6 that help us not only to decouple code but also to come up with constructs that were previously harder to implement, such as the ability of dealing with a sequence of songs indistinctly -- regardless of whether the sequence is finite or infinite. This indifference is, in part, what makes writing code leveraging the iterator protocol more elegant. It also makes it risky to cast an unknown iterable into an array (with, say, the +...+ spread operator), as you're risking crashing your program due to an infinite loop. Generators are an alternative way of creating functions that return an iterable object, without explicitly declaring an object literal with a +Symbol.iterator+ method. They make it easier to implement functions, such as the +range+ or +take+ functions in section 4.2.2, while also allowing for a few more interesting use cases. @@ -1279,7 +1296,7 @@ Yet another benefit of asking consumers to provide a generator function is that ==== 4.3.5 Dealing with asynchronous flows -Staying on the subject of our magic 8-ball, and going back to the example where we call +ball+ with a user-provided +questions+ generator, let's reminisce about what would change about our code if the answers were to be provided asynchronously. The beauty of generators is that if the way we iterate over the questions were to become asynchronous, the generator wouldn't have to change at all. We already have the ability to suspend execution in the generator while we fetch the answers to the questions, and all it'd take would be to ask a service for the answer to the current question, return that value via an intermediary +yield+ statement or in some other way, and then call +g.next+ on the +questions+ generator object. +Staying on the subject of our magic 8-ball, and going back to the example where we call +ball+ with a user-provided +questions+ generator, let's consider what would change about our code if the answers were to be provided asynchronously. The beauty of generators is that if the way we iterate over the questions were to become asynchronous, the generator wouldn't have to change at all. We already have the ability to suspend execution in the generator while we fetch the answers to the questions, and all it'd take would be to ask a service for the answer to the current question, return that value via an intermediary +yield+ statement or in some other way, and then call +g.next+ on the +questions+ generator object. Let's assume we're back at the following usage of +ball+. @@ -1626,7 +1643,7 @@ We've looked into flow control mechanisms such as callbacks, events, promises, i Languages like Python and C# have had +async+ / +await+ for a while. In ES2017, JavaScript gained native syntax that can be used to describe asynchronous operations. -Let's go over a quick recap comparing promises, callbacks and generators. Afterwards we'll look into Async Functions in JavaScript, and how this new feature can help make our code more readable. +Let's go over a quick recap comparing promises, callbacks and generators. Afterwards we'll look into async functions in JavaScript, and how this new feature can help make our code more readable. ==== 4.4.1 Flavors of Async Code @@ -1722,17 +1739,17 @@ getRandomArticle(function* printRandomArticle () { }); ---- -Generators may not be the most straightforward way of accomplishing the results that we want in this case: you're only moving the complexity somewhere else. We might as well stick with Promises. +Generators may not be the most straightforward way of accomplishing the results that we want in this case: you're only moving the complexity somewhere else. We might as well stick with promises. Besides involving an unintuitive syntax into the mix, your iterator code will be highly coupled to the generator function that's being consumed. That means you'll have to change it often as you add new +yield+ expressions to the generator code. -A better alternative would be to use an Async Function. +A better alternative would be to use an async function. ==== 4.4.2 Using +async+ / +await+ -Async Functions let us take a +Promise+-based implementation and take advantage of the synchronous-looking generator style. A huge benefit in this approach is that you won't have to change the original +getRandomArticle+ at all: as long as it returns a promise it can be awaited. +Async functions let us take a promise-based implementation and take advantage of the synchronous-looking generator style. A huge benefit in this approach is that you won't have to change the original +getRandomArticle+ at all: as long as it returns a promise it can be awaited. -Note that +await+ may only be used inside Async Functions, marked with the +async+ keyword. Async Functions work similarly to generators, by suspending execution in the local context until a promise settles. If the awaited expression isn't originally a promise, it gets casted into a promise. +Note that +await+ may only be used inside async functions, marked with the +async+ keyword. Async functions work similarly to generators, by suspending execution in the local context until a promise settles. If the awaited expression isn't originally a promise, it gets casted into a promise. The following piece of code consumes our original +getRandomArticle+, which relied on promises. Then it runs that model through an asynchronous +renderView+ function, which returns a bit of HTML, and updates the page. Note how we can use +try+ / +catch+ to handle errors in awaited promises from within the +async+ function, treating completely asynchronous code as if it were synchronous. @@ -1752,7 +1769,7 @@ async function read () { read() ---- -An Async Function always returns a +Promise+. In the case of uncaught exceptions, the returned promise settles in rejection. Otherwise, the returned promise resolves to the return value. This aspect of Async Functions allows us to mix them with regular promise-based continuation as well. The following example shows how the two may be combined. +An async function always returns a +Promise+. In the case of uncaught exceptions, the returned promise settles in rejection. Otherwise, the returned promise resolves to the return value. This aspect of async functions allows us to mix them with regular promise-based continuation as well. The following example shows how the two may be combined. [source,javascript] ---- @@ -1768,7 +1785,7 @@ read() .catch(err => console.error(err)) ---- -Making the +read+ function a bit more reusable, we could return the resulting +html+, and allow consumers to do continuation using promises or yet another Async Function. That way, your +read+ function becomes only concerned with pulling down the HTML for a view. +Making the +read+ function a bit more reusable, we could return the resulting +html+, and allow consumers to do continuation using promises or yet another async function. That way, your +read+ function becomes only concerned with pulling down the HTML for a view. [source,javascript] ---- @@ -1786,7 +1803,7 @@ Following the example, we can use plain promises to prints the HTML. read().then(html => console.log(html)) ---- -Using Async Functions wouldn't be all that difficult for continuation, either. In the next snippet, we create a +write+ function used for continuation. +Using async functions wouldn't be all that difficult for continuation, either. In the next snippet, we create a +write+ function used for continuation. [source,javascript] ---- @@ -1800,7 +1817,7 @@ What about concurrent asynchronous flows? ==== 4.4.3 Concurrent Async Flows -In asynchronous code flows, it is commonplace to execute two or more tasks concurrently. While Async Functions make it easier to write asynchronous code, they also lend themselves to code that executes one asynchronous operation at a time. A function with multiple +await+ expressions in it will be suspended once at a time on each +await+ expression until that +Promise+ is settled, before unsuspending execution and moving onto the next +await+ expression -- this is a similar case to what we observe with generators and +yield+. +In asynchronous code flows, it is commonplace to execute two or more tasks concurrently. While async functions make it easier to write asynchronous code, they also lend themselves to code that executes one asynchronous operation at a time. A function with multiple +await+ expressions in it will be suspended once at a time on each +await+ expression until that +Promise+ is settled, before unsuspending execution and moving onto the next +await+ expression -- this is a similar case to what we observe with generators and +yield+. [source,javascript] ---- @@ -1830,7 +1847,7 @@ async function concurrent () { } ---- -Promises offer an alternative to +Promise.all+ in +Promise.race+. We can use +Promise.race+ to get the result from the promise that fulfills quicker. +We could use +Promise.race+ to get the result from the promise that fulfills quicker. [source,javascript] ---- @@ -1846,13 +1863,13 @@ async function race () { ==== 4.4.4 Error Handling -Errors are swallowed silently within an +async+ function, just like inside normal Promises, due to Async Functions being wrapped in a +Promise+. Uncaught exceptions raised in the body of your Async Function or during suspended execution while evaluating an +await+ expresion will reject the promise returned by the +async+ function. +Errors are swallowed silently within an +async+ function, just like inside normal Promises, due to async functions being wrapped in a +Promise+. Uncaught exceptions raised in the body of your async function or during suspended execution while evaluating an +await+ expresion will reject the promise returned by the +async+ function. -That is, unless we add +try+ / +catch+ blocks around +await+ expressions. For the portion of the Async Function code that's wrapped, errors are treated under typical +try+ / +catch+ semantics. +That is, unless we add +try+ / +catch+ blocks around +await+ expressions. For the portion of the async function code that's wrapped, errors are treated under typical +try+ / +catch+ semantics. -Naturally, this can be seen as a strength: you can leverage +try+ / +catch+ conventions, something you were unable to do with asynchronous callbacks, and somewhat able to when using promises. In this sense, Async Functions are akin to generators, where we can take advantage of +try+ / +catch+ thanks to function execution suspension turning asynchronous flows into seemingly synchronous code. +Naturally, this can be seen as a strength: you can leverage +try+ / +catch+ conventions, something you were unable to do with asynchronous callbacks, and somewhat able to when using promises. In this sense, async functions are akin to generators, where we can take advantage of +try+ / +catch+ thanks to function execution suspension turning asynchronous flows into seemingly synchronous code. -Furthermore, you're able to catch these exceptions from outside the +async+ function, by adding a +.catch+ clause to the promise they return. While this is a flexible way of combining the +try+ / +catch+ error handling flavor with +.catch+ clauses in Promises, it can also lead to confusion and ultimately cause to errors going unhandled, unless everyone reading the code is comfortable with async function semantics in terms of the promise wrapper and how +try+ / +catch+ works under this context. +Furthermore, you're able to catch these exceptions from outside the +async+ function, by adding a +.catch+ clause to the promise they return. While this is a flexible way of combining the +try+ / +catch+ error handling flavor with +.catch+ clauses in promises, it can also lead to confusion and ultimately cause to errors going unhandled, unless everyone reading the code is comfortable with async function semantics in terms of the promise wrapper and how +try+ / +catch+ works under this context. [source,javascript] ---- @@ -1865,7 +1882,7 @@ As you can see, there's quite a few ways in which we can notice exceptions and t ==== 4.4.5 Understanding Async Function Internals -Async Functions leverage both generators and promises internally. Let's suppose we have the following Async Function. +Async functions leverage both generators and promises internally. Let's suppose we have the following async function. [source,javascript] ---- @@ -1930,7 +1947,7 @@ function spawn (generator) { } ---- -Consider the following Async Function. In order to print the result, we're also using promise-based continuation. Let's follow the code as a thought exercise. +Consider the following async function. In order to print the result, we're also using promise-based continuation. Let's follow the code as a thought exercise. [source,javascript] ---- @@ -1944,7 +1961,7 @@ exercise().then(result => console.log(result)) // <- ['slowest', 'slow'] ---- -First, we could translate the function to our +spawn+ based logic. We wrap the body of our Async Function in a generator passed to +spawn+, and replace any +await+ expressions with +yield+. +First, we could translate the function to our +spawn+ based logic. We wrap the body of our async function in a generator passed to +spawn+, and replace any +await+ expressions with +yield+. [source,javascript] ---- @@ -1960,7 +1977,7 @@ exercise().then(result => console.log(result)) // <- ['slowest', 'slow'] ---- -When +spawn+ is called with the generator function, it immediately creates a generator object and executes +step+ a first time, as seen in the next code snippet. The +step+ function will also be used whenever we reach a +yield+ expression, which are equivalent to the +await+ expressions in our Async Function. +When +spawn+ is called with the generator function, it immediately creates a generator object and executes +step+ a first time, as seen in the next code snippet. The +step+ function will also be used whenever we reach a +yield+ expression, which are equivalent to the +await+ expressions in our async function. [source,javascript] ---- @@ -1976,7 +1993,7 @@ function spawn (generator) { } ---- -The first thing that happens in the +step+ function is calling the +nextFn+ function inside a +try+ / +catch+ block. This resumes execution in the generator function. If the generator function were to produce an error, we'd fall into the +catch+ clause, and the underlying promise for our Async Function would be rejected without any further steps, as shown next. +The first thing that happens in the +step+ function is calling the +nextFn+ function inside a +try+ / +catch+ block. This resumes execution in the generator function. If the generator function were to produce an error, we'd fall into the +catch+ clause, and the underlying promise for our async function would be rejected without any further steps, as shown next. [source,javascript] ---- @@ -1996,16 +2013,16 @@ function runNext (nextFn) { } ---- -Back to the Async Function, code up until the following expression is evaluated. No errors are incurred, and execution in the Async Function is suspended once again. +Back to the async function, code up until the following expression is evaluated. No errors are incurred, and execution in the async function is suspended once again. [source,javascript] ---- yield new Promise(resolve => setTimeout(resolve, 500, 'slowest')) ---- -The yielded expression is received by +step+ as +next.value+, while +next.done+ indicates whether the generator sequence has ended. In this case, we receive the +Promise+ in the function controlling exactly how iteration should occur. At this time, +next.done+ is +false, meaning we won't be resolving the async function's wrapper Promise. We wrap +next.value+ in a fulfilled +Promise+, just in case we haven't received a +Promise+. +The yielded expression is received by +step+ as +next.value+, while +next.done+ indicates whether the generator sequence has ended. In this case, we receive the +Promise+ in the function controlling exactly how iteration should occur. At this time, +next.done+ is +false+, meaning we won't be resolving the async function's wrapper Promise. We wrap +next.value+ in a fulfilled +Promise+, just in case we haven't received a +Promise+. -We then wait on the +Promise+ to be fulfilled or rejected. If the promise is fulfilled, we push the fulfillment value to the generator function by advancing the generator sequence with +value+. If the promise is rejected, we would've used +g.throw+, which would've resulted in an error being raised in the generator function, causing the Async Function's wrapper promise to be rejected at +runNext+. +We then wait on the +Promise+ to be fulfilled or rejected. If the promise is fulfilled, we push the fulfillment value to the generator function by advancing the generator sequence with +value+. If the promise is rejected, we would've used +g.throw+, which would've resulted in an error being raised in the generator function, causing the async function's wrapper promise to be rejected at +runNext+. [source,javascript] ---- @@ -2028,14 +2045,14 @@ function step (nextFn) { Using +g.next()+ on its own means that the generator function resumes execution. By passing a value to +g.next(value)+, we've made it so that the +yield+ expression evaluates to that +value+. The +value+ in question is, in this case, the fulfillment value of the originally yielded +Promise+, which is +'slowest'+. -Back in the generator function, we assign +'slowest' to +r1+. +Back in the generator function, we assign `'slowest'` to `r1`. [source,javascript] ---- const r1 = yield new Promise(resolve => setTimeout(resolve, 500, 'slowest')) ---- -Then, execution runs up until the second +yield+ statement. The +yield+ expression once again causes execution in the Async Function to be suspended, and sends the new +Promise+ to the +spawn+ iterator. +Then, execution runs up until the second +yield+ statement. The +yield+ expression once again causes execution in the async function to be suspended, and sends the new +Promise+ to the +spawn+ iterator. [source,javascript] ---- @@ -2053,15 +2070,15 @@ return [r1, r2] At this point, +next+ evaluates to the following object. -[source,javascript] +[source,json] ---- { - value: ['slowest', 'slow'], - done: true + "value": ["slowest", "slow"], + "done": true } ---- -Immediately, the iterator checks that +next.done+ is indeed +true+, and resolves the Async Function to +['slowest', 'slow']+. +Immediately, the iterator checks that `next.done` is indeed `true`, and resolves the async function to `['slowest', 'slow']`. [source,javascript] ---- @@ -2080,9 +2097,9 @@ exercise().then(result => console.log(result)) // <- ['slowest', 'slow'] ---- -Async Functions, then, are little more than a sensible default when it comes to iterating generator functions in such a way that makes passing values back and forth as frictionless as possible. Some syntactic sugar hides away the generator function, the +spawn+ function used to iterate over the sequence of yielded expressions, and +yield+ becomes +await+. +Async functions, then, are little more than a sensible default when it comes to iterating generator functions in such a way that makes passing values back and forth as frictionless as possible. Some syntactic sugar hides away the generator function, the +spawn+ function used to iterate over the sequence of yielded expressions, and +yield+ becomes +await+. -Noting that Async Functions are syntactic sugar on top of generators and promises, we can also make a point about the importance of learning how each of these constructs work in order to get better insight into how you can mix, match, and combine all the different flavors of asynchronous code flows together. +Noting that async functions are syntactic sugar on top of generators and promises, we can also make a point about the importance of learning how each of these constructs work in order to get better insight into how you can mix, match, and combine all the different flavors of asynchronous code flows together. === 4.5 Asynchronous Iteration @@ -2129,7 +2146,7 @@ The contract for an iterator mandates that the +next+ method of +Symbol.iterator ==== 4.5.1 Async Iterators -In *async iterators*, the contract has a subtle difference: +next+ is supposed to return a +Promise+ that resolves to an object containing +value+ and +done+ properties. The promise enables the sequence to define asynchronous tasks before the next item in the series is resolved. A new +Symbol.asyncIterator+ is introduced to declare asynchronous iterators, in order to avoid confusion that would result of reusing +Symbol.iterator+. +In async iterators, the contract has a subtle difference: +next+ is supposed to return a +Promise+ that resolves to an object containing +value+ and +done+ properties. The promise enables the sequence to define asynchronous tasks before the next item in the series is resolved. A new +Symbol.asyncIterator+ is introduced to declare asynchronous iterators, in order to avoid confusion that would result of reusing +Symbol.iterator+. The +sequence+ iterable could be made compatible with the async iterator interface with two small changes: we replace +Symbol.iterator+ with +Symbol.asyncIterator+, and we wrap the return value for the +next+ method in +Promise.resolve+, thus returning a +Promise+. @@ -2167,7 +2184,7 @@ const interval = duration => ({ }) ---- -In order to consume an async iterator, we can leverage the new +for await..of+ construct introduced alongside Async Iterators. This is yet another way of writing code that behaves asynchronously yet looks synchronous. Note that +for await..of+ statements are only allowed inside Async Functions. +In order to consume an async iterator, we can leverage the new +for await..of+ construct introduced alongside async iterators. This is yet another way of writing code that behaves asynchronously yet looks synchronous. Note that +for await..of+ statements are only allowed inside async functions. [source,javascript] ---- @@ -2196,7 +2213,7 @@ async function* fetchInterval(duration, ...params) { When stepped over, async generators return objects with a +{ next, return, throw }+ signature, whose methods return promises for +{ next, done }+. This is in contrast with regular generators which return +{ next, done }+ directly. -You can consume the +interval+ async generator in exactly the same way you could consume the object-oriented async iterator. The following example consumes the +fetchInterval+ generator to poll an +/api/status+ HTTP resource and leverage its JSON response. After each step ends, we wait for a second and repeat the process. +You can consume the +fetchInterval+ async generator in exactly the same way you could consume the object-based +interval+ async iterator. The following example consumes the +fetchInterval+ generator to poll an +/api/status+ HTTP resource and leverage its JSON response. After each step ends, we wait for a second and repeat the process. [source,javascript] ---- diff --git a/chapters/ch05.asciidoc b/chapters/ch05.asciidoc index 3cc5b49..79b3406 100644 --- a/chapters/ch05.asciidoc +++ b/chapters/ch05.asciidoc @@ -1,7 +1,7 @@ [[leveraging-es-collections]] == Leveraging ECMAScript Collections -JavaScript data structures are flexible enough that we're able to turn any object into a hash-map, where we map string keys to arbitrary values. For example, one might use an object to map +npm+ package names to their metadata, as shown next. +JavaScript data structures are flexible enough that we're able to turn any object into a hash-map, where we map string keys to arbitrary values. For example, one might use an object to map npm package names to their metadata, as shown next. [source,javascript] ---- @@ -273,7 +273,7 @@ console.log([...map]) // <- [['a', 3]] ---- -In ES6 maps, +NaN+ becomes a "corner-case" that gets treated as a value that's equal to itself, even though the +NaN === NaN+ expression evaluates to +false+. A number of ECMAScript features introduced in ES6 and later use a different comparison algorithm than that of ES5 and earlier where +NaN+ is equal to +NaN+, although +NaN !== NaN+; and ++0+ is different from +-0+, even though ++0 === -0+. The following piece of code shows how even though +NaN+ is typically evaluated to be different than itself, +Map+ considers +NaN+ to be a constant value that's always the same. +In ES6 maps, `NaN` becomes a "corner-case" that gets treated as a value that's equal to itself, even though the `NaN === NaN` expression evaluates to `false`. A number of ECMAScript features introduced in ES6 and later use a different comparison algorithm than that of ES5 and earlier where `NaN` is equal to `NaN`, although `NaN !== NaN`; and `+0` is different from `-0`, even though `+0 === -0`. The following piece of code shows how even though `NaN` is typically evaluated to be different than itself, `Map` considers `NaN` to be a constant value that's always the same. [source,javascript] ---- @@ -309,7 +309,7 @@ console.log([...map.entries()]) Map entries are always iterated in insertion order. This contrasts with +Object.keys+, which is specified to follow an arbitrary order. Although, in practice, insertion order is typically preserved by JavaScript engines regardless of the specification. -Maps have a +.forEach+ method that's identical in behavior to that in ES5 +Array+ objects. Once again, keys do not get casted into strings in the case of +Map+, as demonstrated below. +Maps have a +.forEach+ method that's equivalent in behavior to that in ES5 +Array+ objects. The signature is `(value, key, map)`, where `value` and `key` correspond to the current item in the iteration, while `map` is the map being iterated. Once again, keys do not get casted into strings in the case of +Map+, as demonstrated below. [source,javascript] ---- @@ -365,7 +365,9 @@ function destroy (entry) { } ---- -One of the most valuable aspects of +Map+ is the ability to index by any object, such as DOM elements. That, combined with the fact that +Map+ also has collection manipulation abilities greatly simplifies things. +One of the most valuable aspects of +Map+ is the ability to index by any object, such as DOM elements. That, combined with the fact that +Map+ also has collection manipulation abilities greatly simplifies things. This is crucial for DOM manipulation in jQuery and other DOM-heavy libraries, which often need to map DOM elements to their internal state. + +The following example shows how `Map` would reduce the burden of maintenance in user code. [source,javascript] ---- @@ -393,7 +395,7 @@ function destroy (el) { } ---- -The fact that mapping functions have become one liners thanks to native +Map+ methods means we could inline those functions instead, as readability is no longer an issue. The following piece of code is a vastly simplified alternative to the ES5 piece of code we started with. Here we're not concerned with implementation details anymore, but have instead boiled the DOM-to-API mapping to its bare essentials. +The fact that mapping functions have become one-liners thanks to native +Map+ methods means we could inline those functions instead, as readability is no longer an issue. The following piece of code is a vastly simplified alternative to the ES5 piece of code we started with. Here we're not concerned with implementation details anymore, but have instead boiled the DOM-to-API mapping to its bare essentials. [source,javascript] ---- @@ -430,7 +432,7 @@ map.set(Symbol(), 2) // <- TypeError ---- -In exchange for having a more limited feature set, +WeakMap+ key references are weakly held, meaning that the objects referenced by +WeakMap+ keys are subject to garbage collection if there are no other references to them. This kind of behavior is useful when you have metadata about a +person+, for example, but you want the +person+ to be garbage collected when and if the only reference back to +person+ is their metadata. You can now keep that metadata in a +WeakMap+ using +person+ as the key. +In exchange for having a more limited feature set, +WeakMap+ key references are weakly held, meaning that the objects referenced by +WeakMap+ keys are subject to garbage collection if there are no references to them -- other than weak references. This kind of behavior is useful when you have metadata about a +person+, for example, but you want the +person+ to be garbage collected when and if the only reference back to +person+ is their metadata. You can now keep that metadata in a +WeakMap+ using +person+ as the key. With +WeakMap+, you are still able to provide an iterable for initialization. @@ -457,33 +459,33 @@ map.has(date) // <- false ---- -==== 5.2.1 Is +WeakMap+ Strictly Worse Than +Map+? +==== 5.2.1 Is +WeakMap+ A Worse +Map+? -The distinction that makes +WeakMap+ worth the trouble is in its name. Given that +WeakMap+ holds references to its keys weakly, those object are subject to garbage collection if there are no other references to them other than as +WeakMap+ keys. This is in contrast with +Map+ which holds strong object references, preventing +Map+ keys and values from being garbage collected. +The distinction which makes +WeakMap+ worth the trouble is in its name. Given that +WeakMap+ holds references to its keys weakly, those object are subject to garbage collection if there are no other references to them other than as +WeakMap+ keys. This is in contrast with +Map+ which holds strong object references, preventing +Map+ keys and values from being garbage collected. -Correspondingly, use cases for +WeakMap+ revolve around the need to specify metadata or extend an object while still being able to garbage collect that object if there are no other references to it. A perfect example might be the underlying implementation for +process.on('unhandledRejection')+ in Node.js, which uses a +WeakMap+ to keep track of rejected promises that weren't dealt with. By using +WeakMap+, the implementation prevents memory leaks because the +WeakMap+ won't be grabbing onto those promises strongly. In this case, we have a simple map that weakly holds onto promises, but is flexible enough to handle entries being removed from the map when they're no longer referenced anywhere else. +Correspondingly, use cases for +WeakMap+ revolve around the need to specify metadata or extend an object while still being able to garbage collect that object if there are no other references to it. A perfect example might be the underlying implementation for `process.on('unhandledRejection')` in Node.js, which uses a +WeakMap+ to keep track of rejected promises that weren't dealt with yet. By using +WeakMap+, the implementation prevents memory leaks because the +WeakMap+ won't be grabbing onto the state related to those promises strongly. In this case, we have a simple map that weakly holds onto state, but is flexible enough to handle entries being removed from the map when promises are no longer referenced anywhere else. Keeping data about DOM elements that should be released from memory when they're no longer of interest is another important use case, and in this regard using +WeakMap+ is an even better solution to the DOM-related API caching solution we implemented earlier using +Map+. -In so many words, then: no, +WeakMap+ is not strictly worse than +Map+ -- they just cater to different use cases. +In so many words, then: no, +WeakMap+ is definitely not worse than +Map+ -- they just cater to different use cases. === 5.3 Sets in ES6 -A set is a grouping of values. Sets are also a new collection type in ES6. Sets are similar to +Map+. +The `Set` built-in is a new collection type in ES6 used to represent a grouping of values. In several aspects, `Set` is similar to `Map`. -- +Set+ is also iterable -- +Set+ constructor also accepts an iterable -- +Set+ also has a +.size+ property -- Keys can be arbitrary values or object references -- Keys must be unique -- +NaN+ equals +NaN+ when it comes to +Set+ too -- All of +.keys+, +.values+, +.entries+, +.forEach+, +.has+, +.delete+, and +.clear+ +- `Set` is also iterable +- `Set` constructor also accepts an iterable +- `Set` also has a `.size` property +- `Set` values can be arbitrary values or object references, like `Map` keys +- `Set` values must be unique, like `Map` keys +- `NaN` equals `NaN` when it comes to `Set` too +- All of `.keys`, `.values`, `.entries`, `.forEach`, `.has`, `.delete`, and `.clear` -At the same time, sets are different from +Map+ in a few key ways. Sets don't hold key value pairs, there's only one dimension. You can think of sets as being similar to arrays where every element is distinct from each other. +At the same time, sets are different from `Map` in a few key ways. Sets don't hold key value pairs, there's only one dimension. You can think of sets as being similar to arrays where every element is distinct from each other. -There isn't a +.get+ method in +Set+. A +set.get(key)+ method would be redundant: if you already have the +key+ then there isn't anything else to get, as that's the only dimension. If we wanted to check for whether the +key+ is in the set, there's +set.has(key)+ to fulfill that role. +There isn't a `.get` method in `Set`. A `set.get(value)` method would be redundant: if you already have the `value` then there isn't anything else to get, as that's the only dimension. If we wanted to check for whether the `value` is in the set, there's `set.has(value)` to fulfill that role. -Similarly, a +set.set(key)+ method wouldn't be aptly named, as you aren't setting a +value+ to a +key+, but merely adding a value to the set instead. Thus, the method to add values to a set is +set.add+, as demonstrated in the next snippet. +Similarly, a `set.set(value)` method wouldn't be aptly named, as you aren't setting a `key` to a `value`, but merely adding a value to the set instead. Thus, the method to add values to a set is `set.add`, as demonstrated in the next snippet. [source,javascript] ---- @@ -491,7 +493,7 @@ const set = new Set() set.add({ an: 'example' }) ---- -Sets are iterable, but unlike maps you only iterate over keys, not key value pairs. The following example demonstrates how sets can be spread over an array using the spread operator and creating a single dimensional list. +Sets are iterable, but unlike maps you only iterate over values, not key value pairs. The following example demonstrates how sets can be spread over an array using the spread operator and creating a single dimensional list. [source,javascript] ---- @@ -500,7 +502,7 @@ console.log([...set]) // <- ['a', 'b', 'c'] ---- -In the following example you can note how a set won't contain duplicate entries: every element in a +Set+ must be unique. +In the following example you can note how a set won't contain duplicate entries: every element in a `Set` must be unique. [source,javascript] ---- @@ -509,7 +511,7 @@ console.log([...set]) // <- ['a', 'b', 'c'] ---- -The following piece of code creates a +Set+ with all of the +