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 +
+ elements on a page and then prints how many were found. Then, we query the DOM again and call +set.add+ again for every DOM element. Given that they're all already in the +set+, the +.size+ property won't change, meaning the +set+ remains the same. +The following piece of code creates a `Set` with all of the `
` elements on a page and then prints how many were found. Then, we query the DOM again and call `set.add` again for every DOM element. Given that they're all already in the `set`, the `.size` property won't change, meaning the `set` remains the same. [source,javascript] ---- @@ -524,11 +526,45 @@ console.log(set.size) // <- 56 ---- +Given that a `Set` has no keys, the `Set#entries` method returns an iterator of `[value, value]` for each element in the set. + +[source,javascript] +---- +const set = new Set(['a', 'b', 'c']) +console.log([...set.entries()]) +// <- [['a', 'a'], ['b', 'b'], ['c', 'c']] +---- + +The `Set#entries` method is consistent with `Map#entries`, which returns an iterator of `[key, value]` pairs. Using `Set#entries` as the default iterator for `Set` collections wouldn't be valuable, since it's used in `for..of`, when spreading a `set`, and in `Array.from`. In all of those cases, you probably want to iterate over a sequence of values in the set, but not a sequence of `[value, value]` pairs. + +As demonstrated next, the default `Set` iterator uses `Set#values`, as opposed to `Map` which defined its iterator as `Map#entries`. + +[source,javascript] +---- +const map = new Map() +console.log(map[Symbol.iterator] === map.entries) +// <- true +const set = new Set() +console.log(set[Symbol.iterator] === set.entries) +// <- false +console.log(set[Symbol.iterator] === set.values) +// <- true +---- + +The `Set#keys` method also returns an iterator for values, again for consistency, and it's in fact a reference to the `Set#values` iterator. + +[source,javascript] +---- +const set = new Set() +console.log(set.keys === set.values) +// <- true +---- + === 5.4 ES6 WeakSets -In a similar fashion to +Map+ and +WeakMap+, +WeakSet+ is the weak version of +Set+ that can't be iterated over. You can't iterate over a +WeakSet+. The values in a +WeakSet+ must be unique object references. If nothing else is referencing a +value+ found in a +WeakSet+, it'll be subject to garbage collection. +In a similar fashion to `Map` and `WeakMap`, `WeakSet` is the weak version of `Set` that can't be iterated over. The values in a `WeakSet` must be unique object references. If nothing else is referencing a `value` found in a `WeakSet`, it'll be subject to garbage collection. -Much like in +WeakMap+, you can only +.add+, +.delete+, and check if +WeakSet#has+ a given +value+. Just like in +Set+, there's no +.get+ because sets are one-dimensional. +Much like in `WeakMap`, you can only `.add`, `.delete`, and check if `WeakSet#has` a given `value`. Just like in `Set`, there's no `.get` because sets are one-dimensional. We aren't allowed to add primitive values such as strings or symbols to a +WeakSet+. diff --git a/chapters/ch06.asciidoc b/chapters/ch06.asciidoc index 7094710..2336a8f 100644 --- a/chapters/ch06.asciidoc +++ b/chapters/ch06.asciidoc @@ -1,11 +1,11 @@ [[managing-property-access-with-proxies]] == Managing Property Access with Proxies -Proxies are an interesting and powerful feature coming in ES6 that act as intermediaries between API consumers and objects. In a nutshell, you can use a +Proxy+ to determine the desired behavior whenever the properties of an underlying +target+ object are accessed. A +handler+ object can be used to configure traps for your +Proxy+, which define and restrict how the underlying object is accessed, as we'll see in a bit. +Proxies are an interesting and powerful feature in ES6 that act as intermediaries between API consumers and objects. In a nutshell, you can use a +Proxy+ to determine the desired behavior whenever the properties of an underlying +target+ object are accessed. A +handler+ object can be used to configure traps for your +Proxy+, which define and restrict how the underlying object is accessed, as we'll see in a bit. === 6.1 Getting Started with Proxy -By default, proxies don't do much -- in fact they don't do anything. If you don't provide any configuration, your +proxy+ will just work as a pass-through to the +target+ object, also known as a "no-op forwarding +Proxy+" meaning that all operations on the +Proxy+ object defer to the underlying object. +By default, proxies don't do much -- in fact they don't do anything. If you don't provide any configuration, your proxy will just work as a pass-through to the +target+ object, also known as a "no-op forwarding proxy" meaning that all operations on the proxy object defer to the underlying object. In the following piece of code, we create a no-op forwarding +Proxy+. You can observe how by assigning a value to +proxy.exposed+, that value is passed onto +target.exposed+. You could think of proxies as the gatekeepers of their underlying objects: they may allow certain operations to go through and prevent others from passing, but they carefully inspect every single interaction with their underlying objects. @@ -46,9 +46,9 @@ proxy['something-else'] // <- undefined ---- -As a complement to proxies, ES6 introduces a +Reflect+ built-in object. The traps in ES6 proxies are mapped one-to-one to the +Reflect+ API: For every trap, there’s a matching reflection method in +Reflect+. These methods can be particularly useful when we want to provide the default behavior of proxy traps, but we don't want to concern ourselves with the implementation of that behavior. +As a complement to proxies, ES6 introduces a +Reflect+ built-in object. The traps in ES6 proxies are mapped one-to-one to the +Reflect+ API: For every trap, there’s a matching reflection method in +Reflect+. These methods can be particularly useful when we want the default behavior of proxy traps, but we don't want to concern ourselves with the implementation of that behavior. -In the following code snippet we use +Reflect.get+ to provide the default behavior for +get+ operations, while not worrying about accessing the +key+ property in +target+ by hand. While in this case the operation may seem trivial, the default behavior for other traps may be harder to remember and implement correctly. However, when using the +Reflect+ API, we just need to forward the method call to the reflection API and return the result. +In the following code snippet we use +Reflect.get+ to provide the default behavior for +get+ operations, while not worrying about accessing the +key+ property in +target+ by hand. While in this case the operation may seem trivial, the default behavior for other traps may be harder to remember and implement correctly. We can forward every parameter in the trap to the reflection API and return its result. [source,javascript] ---- @@ -68,8 +68,7 @@ The +get+ trap doesn't necessarily have to return the original +target[key]+ val ---- const handler = { get (target, key) { - const [prefix] = key - if (prefix === '_') { + if (key.startsWith('_')) { throw new Error(`Property "${ key }" cannot be read through this proxy.`) } return Reflect.get(target, key) @@ -182,7 +181,7 @@ const proxy = concealWithPrefix(target) // expose proxy to consumers ---- -You might be tempted to argue that you could achieve the same behavior in ES5 simply by using variables privately scoped to the +concealWithPrefix+ function, without the need for the +Proxy+ itself. The difference is that proxies allow you to "privatize" property access dynamically. Without relying on +Proxy+, you couldn't mark every property that starts with an underscore as private. You could use +Object.freeze+ on the object, but then you wouldn't be able to modify the properties yourself, either. Or you could define get and set accessors for every property, but then again you wouldn't be able to block access on every single property, only the ones you explicitly configured getters and setters for. +You might be tempted to argue that you could achieve the same behavior in ES5 simply by using variables privately scoped to the +concealWithPrefix+ function, without the need for the +Proxy+ itself. The difference is that proxies allow you to "privatize" property access dynamically. Without relying on +Proxy+, you couldn't mark every property that starts with an underscore as private. You could use +Object.freeze+footnote:[The Object.freeze method prevents adding new properties, removing existing ones, and modifying property value references. Note that it doesn't make the values themselves immutable: their properties can still change provided Object.freeze isn't called on those objects as well.] on the object, but then you wouldn't be able to modify the property references yourself, either. Or you could define get and set accessors for every property, but then again you wouldn't be able to block access on every single property, only the ones you explicitly configured getters and setters for. ==== 6.1.3 Schema Validation with Proxies @@ -190,7 +189,7 @@ Sometimes we have an object with user input that we want to validate against a s There is a number of ways in which you could do schema validation. You could use a validation function that throws errors if an invalid value is found on the object, but you'd have to ensure the object is off limits once you've deemed it valid. You could validate each property individually, but you'd have to remember to validate them whenever they're changed. You could also use a +Proxy+. By providing consumers with a +Proxy+ to the actual model object, you'd ensure that the object never enters an invalid state, as an exception would be thrown otherwise. -Another aspect of schema validation via +Proxy+ is that it helps you separate validation concerns from the +target+ object, where validation occurs sometimes in the wild. The +target+ object would stay as a plain old JavaScript object (or POJO, for short), meaning that while you give consumers a validating proxy, you keep an untainted version of the data that's always valid, as guaranteed by the proxy. +Another aspect of schema validation via +Proxy+ is that it helps you separate validation concerns from the +target+ object, where validation occurs sometimes in the wild. The +target+ object would stay as a plain JavaScript object, meaning that while you give consumers a validating proxy, you keep an untainted version of the data that's always valid, as guaranteed by the proxy. Just like a validation function, the handler settings can be reutilized across several +Proxy+ instances, without having to rely on prototypal inheritance or ES6 classes. @@ -292,7 +291,7 @@ We've already covered +get+, which traps property access; and +set+, which traps ==== 6.3.1 +has+ Trap -We can use +handler.has+ to conceal any property you want. It's a trap for the +in+ operator. In the +set+ trap code samples we prevented changes and even access to properties with a certain prefix, but unwanted accessors could still probe the +proxy+ to figure out whether these properties exist. There are three alternatives here. +We can use +handler.has+ to conceal any property you want when it comes to the `in` operator. In the +set+ trap code samples we prevented changes and even access to properties with a certain prefix, but unwanted accessors could still probe the +proxy+ to figure out whether these properties exist. There are three alternatives here. - Do nothing, in which case +key in proxy+ falls through to +Reflect.has(target, key)+, the equivalent of +key in target+ - Return +true+ or +false+ regardless of whether +key+ is or is not present in +target+ @@ -348,6 +347,16 @@ console.log('_secret' in target) We could've thrown an exception instead. That would be useful in situations where attempts to access properties in the private space is seen as a mistake that would've resulted in an invalid state, rather than as a security concern in code that aims to be embedded into third party websites. +Note that if we wanted to prevent `Object#hasOwnProperty` from finding properties in the private space, the `has` trap won't help. + +[source,javascript] +---- +console.log(proxy.hasOwnProperty('_secret')) +// <- true +---- + +The `getOwnPropertyDescriptor` trap in section 6.4.1 offers a solution that's able to intercept `Object#hasOwnProperty` as well. + ==== 6.3.2 +deleteProperty+ Trap Setting a property to +undefined+ clears its value, but the property is still part of the object. Using the +delete+ operator on a property with code like +delete cat.furBall+ means that the +furBall+ property will be completely gone from the +cat+ object. @@ -419,7 +428,7 @@ Consumers interacting with +target+ through the +proxy+ can no longer delete pro ==== 6.3.3 +defineProperty+ Trap -The +Object.defineProperty+ function can be used to add new properties to a +target+ object, using a property +key+ and a property +descriptor+. For the most part, +Object.defineProperty(target, key, descriptor)+ is used in two kinds of situations. +The +Object.defineProperty+ function -- introduced in ES5 -- can be used to add new properties to a +target+ object, using a property +key+ and a property +descriptor+. For the most part, +Object.defineProperty(target, key, descriptor)+ is used in two kinds of situations. 1. When we need to ensure cross-browser support of getters and setters 2. When we want to define a custom property accessor @@ -577,7 +586,7 @@ for (const key of Object.keys(proxy)) { } ---- -Symbol iteration wouldn't be affected by our +handler+ because Symbol keys have a type of +'symbol'+, which would cause our +.filter+ function to return true. +Symbol iteration wouldn't be affected by our +handler+ because `Symbol` keys have a type of +'symbol'+, which would cause our +.filter+ function to return true. [source,javascript] ---- @@ -650,6 +659,16 @@ console.log(Object.getOwnPropertyDescriptor(proxy, 'topping')) // <- { value: 'mozzarella', writable: true, enumerable: true, configurable: true } ---- +The `getOwnPropertyDescriptor` trap is able to intercept the implementation of `Object#hasOwnProperty`, which relies on property descriptors to check whether a property exists. + +[source,javascript] +---- +console.log(proxy.hasOwnProperty('topping')) +// <- true +console.log(proxy.hasOwnProperty('_secret')) +// <- false +---- + When you're trying to hide things, it's best to have them try and behave as if they fell in some other category than the category they're actually in, thus concealing their behavior and passing it off for something else. Throwing, however, sends the wrong message when we want to conceal something: why does a property throw instead of return +undefined+? It must exist but be inaccessible. This is not unlike situations in HTTP API design where we might prefer to return "404 Not Found" responses for sensitive resources, such as an administration back end, when the user is unauthorized to access them, instead of the technically correct "401 Unauthorized" status code. When debugging concerns outweight security concerns, you should at least consider the +throw+ statement. In any case, it's important to understand your use case in order to figure out the optimal and least surprising behavior for a given component. @@ -671,7 +690,7 @@ The +apply+ trap receives three arguments. - +target+ is the function being proxied - +ctx+ is the context passed as +this+ to +target+ when applying a call -- +args+ is the arguments passed to +target+ when applying the call +- +args+ is an array of arguments passed to +target+ when applying the call The default implementation that doesn't alter the outcome would return the results of calling +Reflect.apply+. @@ -872,8 +891,8 @@ Note that arrow functions can't be used as constructors, and thus we can't use t We can use the +handler.getPrototypeOf+ method as a trap for all of the following operations. -- `Object.prototype.__proto__` property -- `Object.prototype.isPrototypeOf` method +- `Object#__proto__` property +- `Object#isPrototypeOf` method - `Object.getPrototypeOf` method - `Reflect.getPrototypeOf` method - `instanceof` operator @@ -972,25 +991,15 @@ proxy.setPrototypeOf(proxy, base) // <- Error: Changing the prototype is forbidden ---- -In these cases, it's best to fail with an exception so that consumers can understand what is going on. By explicitly disallowing prototype changes, the consumer can start looking elsewhere. If we didn't throw an exception, the consumer could still eventually learn that the prototype isn't changing through debugging. You may as well save them from the pain! - -==== 6.4.6 +isExtensible+ Trap - -An extensible object is an object that you can add new properties to, an object you can extend. - -The +handler.isExtensible+ method can be used for logging or auditing calls to +Object.isExtensible+, but not to decide whether an object is extensible. That's because this trap is subject to a harsh invariant that puts a hard limit to what you can do with it: a +TypeError+ is thrown if +Object.isExtensible(proxy) !== Object.isExtensible(target)+. - -If you didn't want consumers to know whether the underlying object is extensible or not, you could +throw+ an error in an +isExtensible+ trap. +In these cases, it's best to fail with an exception so that consumers can understand what is going on. By explicitly disallowing prototype changes, the consumer can start looking elsewhere. If we didn't throw an exception, the consumer could still eventually learn that the prototype isn't changing through debugging. You might as well save them from that pain! -While this trap is nearly useless, other than for auditing purposes, the hard invariant makes sense because there's also the +preventExtensions+ trap that's a bit more permissive. +==== 6.4.6 +preventExtensions+ Trap -==== 6.4.7 +preventExtensions+ Trap - -You can use +handler.preventExtensions+ to trap the +Object.preventExtensions+ method. When extensions are prevented on an object, new properties can't be added any longer: the object can't be extended. +You can use +handler.preventExtensions+ to trap the +Object.preventExtensions+ method introduced in ES5. When extensions are prevented on an object, new properties can't be added any longer: the object can't be extended. Imagine a scenario where you want to be able to selectively +preventExtensions+ on some objects, but not all of them. In that scenario, you could use a +WeakSet+ to keep track of the objects that should be extensible. If an object is in the set, then the +preventExtensions+ trap should be able to capture those requests and discard them. -The following snippet does exactly that: it keeps objects that can be extended in a +WeakSet+ and prevents the rest from being extended. Note that the trap always returns the opposite of +Reflect.isExtensible(target)+. Returning +true+ means the object can't be extended anymore, while +false+ means the object can still be extended. +The following snippet does exactly that: it keeps objects that can be extended in a +WeakSet+ and prevents the rest from being extended. [source,javascript] ---- @@ -1001,7 +1010,7 @@ const handler = { if (canPrevent) { Object.preventExtensions(target) } - return !Reflect.isExtensible(target) + return Reflect.preventExtensions(target) } } ---- @@ -1030,6 +1039,14 @@ console.log(Object.isExtensible(proxy)) // <- false ---- +==== 6.4.7 +isExtensible+ Trap + +An extensible object is an object that you can add new properties to, an object you can extend. + +The +handler.isExtensible+ method can be used for logging or auditing calls to +Object.isExtensible+, but not to decide whether an object is extensible. That's because this trap is subject to a harsh invariant that puts a hard limit to what you can do with it: a +TypeError+ is thrown if +Object.isExtensible(proxy) !== Object.isExtensible(target)+. + +While this trap is nearly useless other than for auditing purposes, you could also throw an error within the handler if you don't want consumers to know whether the underlying object is extensible or not. + As we've learned over the last few pages, there's a myriad of use cases for proxies. We can use +Proxy+ for all of the following, and that's just the tip of the iceberg. - Add validation rules on plain old JavaScript objects, and enforce them diff --git a/chapters/ch07.asciidoc b/chapters/ch07.asciidoc index 86cec0e..fb81a7a 100644 --- a/chapters/ch07.asciidoc +++ b/chapters/ch07.asciidoc @@ -47,7 +47,7 @@ When ES5 came around, the default radix in +parseInt+ changed, from +8+ to +10+. [source,javascript] ---- -console.log(parseInt(`100`, `8`)) +console.log(parseInt(`100`, 8)) // <- 64 ---- @@ -236,7 +236,7 @@ parseInt(`0o110`.slice(2), 8) // <- 72 ---- -To make matters even worse, the +Number+ function is perfectly able to cast these strings into the correct numbers. +In contrast, the +Number+ function is perfectly able to cast these strings into the correct numbers. [source,javascript] ---- @@ -960,18 +960,18 @@ The +ToInteger+ function translates any values in the +(-1, 0)+ range into +-0+. // <- Uncaught RangeError: Invalid count value ---- -An example use case for +String#repeat+ may be the typical padding function. The +leftPad+ function in the next code snippet takes a multiline string and pads every line with as many +spaces+ as desired, using a default of two spaces. +An example use case for +String#repeat+ may be the typical padding function. The +indent+ function in the next code snippet takes a multiline string and indents every line with as many +spaces+ as desired, using a default of two spaces. [source,javascript] ---- -function leftPad (text, spaces = 2) { +function indent (text, spaces = 2) { return text .split(`\n`) .map(line => ` `.repeat(spaces) + line) .join(`\n`) } -leftPad(`a +indent(`a b c`, 2) // <- ` a\n b\n c` @@ -979,7 +979,7 @@ c`, 2) ==== 7.3.5 String Padding and Trimming -At the time of this writing, there's two new string padding methods slated for publication in ES2017: +String#padStart+ and +String#padEnd+. Using these methods, we wouldn't have to implement something like +leftPad+ in the previous code snippet. When performing string manipulation, we often want to pad a string so that it's formatted consistently with a style we have in mind. This can be useful when formatting numbers, currency, HTML, and in a variety of other cases usually involving monospaced text. +At the time of this writing, there's two new string padding methods slated for publication in ES2017: +String#padStart+ and +String#padEnd+. Using these methods, we wouldn't have to implement something like +indent+ in the previous code snippet. When performing string manipulation, we often want to pad a string so that it's formatted consistently with a style we have in mind. This can be useful when formatting numbers, currency, HTML, and in a variety of other cases usually involving monospaced text. Using +padStart+, we will specify the desired length for the target string and the padding string, which defaults to a single space character. If the original string is at least as long as the specified length, +padStart+ will result in a null operation, returning the original string unchanged. @@ -1048,7 +1048,7 @@ Let's switch protocols and learn about Unicode. ==== 7.3.6 Unicode -JavaScript strings are represented using UTF-16 code unitsfootnote:[Learn more about UCS-2, UCS-4, UTF-16 and UTF-32 here: https://mjavascript.com/out/unicode-encodings.]. Each code unit can be used to represent a code point in the +[U+0000, U+FFFF]+ range -- also known as the BMP, short for basic multilingual plane. You can represent individual code points in the BMP plane using the +`\u3456`+ syntax. You could also represent code units in the +[U+0000, U+0255]+ using the +\x00..\xff+ notation. For instance, +`\xbb`+ represents +`»`+, the +187+ character, as you can verify by doing +parseInt(`bb`, 16)+ -- or +String.fromCharCode(187)+. +JavaScript strings are represented using UTF-16 code unitsfootnote:[Learn more about UCS-2, UCS-4, UTF-16 and UTF-32 here: https://mjavascript.com/out/unicode-encodings.]. Each code unit can be used to represent a code point in the +[U+0000, U+FFFF]+ range -- also known as the BMP, short for basic multilingual plane. You can represent individual code points in the BMP plane using the +`\u3456`+ syntax. You could also represent code units in the +[U+0000, U+0255]+ range using the +\x00..\xff+ notation. For instance, +`\xbb`+ represents +`»`+, the +187+ character, as you can verify by doing +parseInt(`bb`, 16)+ -- or +String.fromCharCode(187)+. For code points beyond +U+FFFF+, you'd represent them as a surrogate pair. That is to say, two contiguous code units. For instance, the horse emoji +`🐎`+ code point is represented with the +`\ud83d\udc0e`+ contiguous code units. In ES6 notation you can also represent code points using the +`\u{1f40e}`+ notation (that example is also the horse emoji). @@ -1198,7 +1198,7 @@ const text = `\ud83d\udc0e\ud83d\udc71\u2764` // <- [128014, 128113, 10084] ---- -You can take the base-16 representation of those base-10 code points, and use them to create a string with the new unicode code point escape syntax of +\u{codePoint}+. This syntax allows you to represent unicode code points that are beyond the BMP (or basic multilingual plane). That is, code points outside the +[U+0000, U+FFFF]+ range that are typically represented using the +\u1234+ syntax. +You can take the base-16 representation of those base-10 code points, and use them to create a string with the new unicode code point escape syntax of +\u{codePoint}+. This syntax allows you to represent unicode code points that are beyond the BMP. That is, code points outside the +[U+0000, U+FFFF]+ range that are typically represented using the +\u1234+ syntax. Let's start by updating our example to print the hexadecimal version of our code points. diff --git a/chapters/ch08.asciidoc b/chapters/ch08.asciidoc index bd63988..7523428 100644 --- a/chapters/ch08.asciidoc +++ b/chapters/ch08.asciidoc @@ -152,7 +152,7 @@ console.log(render(`list`, [{ image::../images/c08g03-dynamic-render.png["Printing different views through a normalized render function."] -Moving on, you'll notice that ES6 modules are heavily influenced by CommonJS. In the next few sections we'll look at +export+ and +import+ statements, and learn how ESM is compatible with CJS. +Moving on, you'll notice that ES6 modules are somewhat influenced by CommonJS. In the next few sections we'll look at +export+ and +import+ statements, and learn how ESM is compatible with CJS. === 8.2 JavaScript Modules @@ -259,7 +259,7 @@ When you want to expose multiple values from CJS modules you don't necessarily n [source,javascript] ---- module.exports.counter = 0 -module.exports.count = () => counter++ +module.exports.count = () => module.exports.counter++ ---- We can replicate this behavior in ESM by using the named exports syntax. Instead of assigning properties to an implicit +module.exports+ object like with CommonJS, in ES6 you declare the bindings you want to +export+, as shown in the following code snippet. @@ -325,17 +325,34 @@ It's important to keep in mind that we are exporting bindings, and not merely va ===== Bindings, Not Values -ES6 modules export bindings, not values nor references. This means that a +counter+ variable you export would be bound into the +counter+ variable on the module, and its value would be subject to changes made to +counter+. While unexpectedly changing the public interface of a module after it has initially loaded can lead to confusion, this can indeed be useful in some cases. +ES6 modules export bindings, not values nor references. This means that a +fungible+ binding exported from a module would be bound into the +fungible+ variable on the module, and its value would be subject to changes made to +fungible+. While unexpectedly changing the public interface of a module after it has initially loaded can lead to confusion, this can indeed be useful in some cases. -In the next code snippet, our module's +counter+ export would be initially bound to +0+ and increase by +1+ every second. Modules consuming this API would see the +counter+ value changing every second. +In the next code snippet, our module's +fungible+ export would be initially bound to an object and be changed into an array after five seconds. [source,javascript] ---- -export let counter = 0 -setInterval(() => counter++, 1000) +export let fungible = { name: 'bound' } +setTimeout(() => fungible = [0, 1, 2], 5000) +---- + +Modules consuming this API would see the +fungible+ value changing after five seconds. Consider the following example, where we print the consumed binding every 2 seconds. + +[source,javascript] +---- +import { fungible } from './fungible' + +console.log(fungible) // <- { name: 'bound' } +setInterval(() => console.log(fungible), 2000) +// <- { name: 'bound' } +// <- { name: 'bound' } +// <- [0, 1, 2] +// <- [0, 1, 2] +// <- [0, 1, 2] ---- -Finally, the JavaScript module system offers an +export..from+ syntax, where you can expose another module's interface. +This kind of behavior is best suited for counters and flags, but is best avoided unless its purpose is clearly defined, since it can lead to confusing behavior and API surfaces changing unexpected from the point of view of a consumer. + +The JavaScript module system also offers an +export..from+ syntax, where you can expose another module's interface. ===== Exporting from another module @@ -568,7 +585,7 @@ const random = (function() { })() ---- -Compare that to the following piece of code, used in an ESM module called +random+. The immediately-invoking function expression wrapper trick went away, along with the name for our module, which now resides in its filename. We've regained the simplicity from back in the day, when we wrote raw JavaScript inside plain HTML +