├── .editorconfig ├── .gitignore ├── atlas.json ├── author_bio.html ├── ch01.asciidoc ├── ch02.asciidoc ├── ch03.asciidoc ├── ch04.asciidoc ├── ch05.asciidoc ├── ch06.asciidoc ├── ch07.asciidoc ├── ch08.asciidoc ├── ch09.asciidoc ├── code ├── ch01 │ ├── ex01-babel-setup │ │ ├── .babelrc │ │ ├── package.json │ │ └── src │ │ │ └── example.js │ └── ex02-eslint-setup │ │ ├── .eslintrc.json │ │ ├── package.json │ │ └── src │ │ └── example.js └── ch08 │ ├── ex01-cjs-grocery-item │ ├── app.js │ └── views │ │ └── item.js │ ├── ex02-cjs-grocery-list │ ├── app.js │ └── views │ │ ├── item.js │ │ └── list.js │ ├── ex03-cjs-dynamic-render │ ├── app.js │ ├── render.js │ └── views │ │ ├── item.js │ │ └── list.js │ ├── ex04-esm-import-default │ ├── .babelrc │ ├── app.js │ ├── counter.js │ └── package.json │ └── ex05-esm-import-named │ ├── .babelrc │ ├── app.js │ ├── counter.js │ └── package.json ├── colo.html ├── contributing.md ├── copyright.html ├── cover.html ├── foreword.asciidoc ├── images ├── 1f40e.png ├── 1f471.png ├── 2764.png ├── a123.png ├── b456.png ├── c789.png ├── cover.png ├── d0123.png ├── e456.png ├── pmjs_0101.png ├── pmjs_0102.png ├── pmjs_0301.png ├── pmjs_0401.png ├── pmjs_0402.png ├── pmjs_0403.png ├── pmjs_0404.png ├── pmjs_0405.png ├── pmjs_0801.png ├── pmjs_0802.png └── pmjs_0803.png ├── ix.html ├── license.md ├── praise.html ├── preface.asciidoc ├── readme.md ├── theme ├── epub │ ├── epub.css │ ├── epub.xsl │ └── layout.html ├── html │ └── html.css ├── mobi │ ├── layout.html │ ├── mobi.css │ └── mobi.xsl └── pdf │ ├── pdf.css │ └── pdf.xsl ├── titlepage.html ├── toc.html └── tools └── intakereport.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | temp/ 4 | .DS_Store 5 | *.log 6 | -------------------------------------------------------------------------------- /atlas.json: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master", 3 | "files": [ 4 | "cover.html", 5 | "praise.html", 6 | "titlepage.html", 7 | "copyright.html", 8 | "toc.html", 9 | "foreword.asciidoc", 10 | "preface.asciidoc", 11 | "ch01.asciidoc", 12 | "ch02.asciidoc", 13 | "ch03.asciidoc", 14 | "ch04.asciidoc", 15 | "ch05.asciidoc", 16 | "ch06.asciidoc", 17 | "ch07.asciidoc", 18 | "ch08.asciidoc", 19 | "ch09.asciidoc", 20 | "ix.html", 21 | "author_bio.html", 22 | "colo.html" 23 | ], 24 | "formats": { 25 | "pdf": { 26 | "version": "print", 27 | "toc": true, 28 | "index": true, 29 | "syntaxhighlighting": true, 30 | "show_comments": false, 31 | "color_count": "1", 32 | "trim_size": "6inx9in", 33 | "antennahouse_version": "AHFormatterV62_64-MR4" 34 | }, 35 | "epub": { 36 | "toc": true, 37 | "index": true, 38 | "syntaxhighlighting": true, 39 | "epubcheck": true, 40 | "show_comments": false, 41 | "downsample_images": false 42 | }, 43 | "mobi": { 44 | "toc": true, 45 | "index": true, 46 | "syntaxhighlighting": true, 47 | "show_comments": false, 48 | "downsample_images": false 49 | }, 50 | "html": { 51 | "toc": true, 52 | "index": true, 53 | "syntaxhighlighting": true, 54 | "show_comments": false, 55 | "consolidated": false 56 | } 57 | }, 58 | "theme": "oreillymedia/animal_theme_sass", 59 | "title": "Practical Modern JavaScript", 60 | "export_formats": [ 61 | "html", 62 | "pdf" 63 | ], 64 | "name": "oreillymedia/practical-modern-javascript", 65 | "templating": false, 66 | "print_isbn13": "9781491943533", 67 | "lang": "en", 68 | "accent_color": "cmyk(100%, 3%, 50%, 0%)" 69 | } -------------------------------------------------------------------------------- /author_bio.html: -------------------------------------------------------------------------------- 1 |
2 |

About the Author

3 |

Nicolás Bevacqua is the author of JavaScript Application Design (Manning). He’s a JavaScript hacker based in Buenos Aires, Argentina. An avid writer, Nicolás is an open source advocate working at Elastic as a UI engineer. You can find his essays about the web on ponyfoo.com.

4 |

He enjoys travelling around the world and speaking at tech conferences.

5 |
6 | -------------------------------------------------------------------------------- /ch01.asciidoc: -------------------------------------------------------------------------------- 1 | [role="pagenumrestart"] 2 | [[ecmascript-and-the-future-of-javascript]] 3 | == ECMAScript and the pass:[Future of JavaScript] 4 | 5 | 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. 6 | 7 | How did JavaScript get here, and where is it going next? 8 | 9 | === A Brief History of JavaScript Standards 10 | 11 | Back ((("JavaScript", "standards evolution", id="js1se")))((("standards evolution", id="se1")))in 1995, ((("Netscape", id="net1")))Netscape envisioned a dynamic web beyond what HTML could offer. Brendan Eich was initially brought into Netscape to develop a language that was functionally akin to Scheme, but for the browser. Once he joined, he learned that upper management wanted it to look like Java, and a deal to that effect was already underway. 12 | 13 | Brendan created the first JavaScript prototype in 10 days, taking Scheme's first-class functions and Self's prototypes as its main ingredients. The initial version of JavaScript was code-named Mocha. It didn't have array or object literals, and every error resulted in an alert. The lack of exception handling is why, to this day, many operations result in `NaN` or `undefined`. Brendan's work on DOM level 0 and the first edition of JavaScript set the stage for standards work. 14 | 15 | This revision of JavaScript was marketed as ((("LiveScript")))LiveScript when it started shipping with a beta release of Netscape Navigator 2.0, in September 1995. It was rebranded as JavaScript (trademarked by Sun, now owned by Oracle) when Navigator 2.0 beta 3 was released in December 1995. Soon after this release, Netscape introduced a server-side JavaScript implementation for scripting in Netscape Enterprise Server, and named it ((("LiveWire")))LiveWire.pass:[A booklet from 1998 explains the intricacies of server-side JavaScript with LiveWire.] ((("JScript")))JScript, Microsoft's reverse-engineered implementation of JavaScript, was bundled with IE3 in 1996. JScript was available for Internet Information Server (IIS) in the server side. 16 | 17 | The language started being standardized under the ((("ECMAScript (ES)", "evolution of", id="ec1eo")))ECMAScript name (ES) into the ECMA-262 specification in 1996, under a technical committee at ECMA known as ((("TC39 standards committee", id="tc391")))TC39. Sun wouldn't transfer ownership of the JavaScript trademark to ECMA, and while Microsoft offered JScript, other member companies didn't want to use that name, so ECMAScript stuck. 18 | 19 | Disputes by competing implementations, JavaScript by Netscape and JScript by ((("Microsoft", id="ms1")))Microsoft, dominated most of the TC39 standards committee meetings at the time. Even so, the committee was already bearing fruit: backward compatibility was established as a golden rule, bringing about strict equality operators (`===` and `!==`) instead of breaking existing programs that relied on the loose Equality Comparison Algorithm. 20 | 21 | The first edition of ECMA-262 was released June 1997. A year later, in June 1998, the specification was refined under the ISO/IEC 16262 international standard, after much scrutiny from national ISO bodies, and formalized as the second edition. 22 | 23 | By December 1999 the third edition was published, standardizing regular expressions, the `switch` statement, `do`/`while`, `try`/`catch`, and `Object#hasOwnProperty`, among a few other changes. Most of these features were already available in the wild through Netscape's JavaScript runtime, ((("SpiderMonkey")))SpiderMonkey. 24 | 25 | Drafts for an ES4 specification were soon afterwards published by TC39. This early work on ES4 led to JScript​.NET in mid-2000pass:[You can read the original announcement at the Microsoft website (July, 2000).] and, eventually, to ActionScript 3 for Flash in 2006.pass:[Listen to Brendan Eich in the JavaScript Jabber podcast, talking about the origin of JavaScript.] 26 | 27 | Conflicting opinions on how JavaScript was to move forward brought work on the specification to a standstill. This was a delicate time for web standards: Microsoft had all but monopolized the web and they had little interest in standards development. 28 | 29 | As AOL laid off 50 Netscape employees in 2003,pass:[You can read a news report from The Mac Observer, July 2003.] the Mozilla Foundation was formed. With over 95% of web-browsing market share now in the hands of ((("Microsoft", startref="ms1")))Microsoft, TC39 was ((("Netscape", startref="net1")))disbanded. 30 | 31 | It took two years until Brendan, now at ((("Mozilla")))Mozilla, had ECMA resurrect work on TC39 by using Firefox's growing market share as leverage to get Microsoft back in the fold. By mid-2005, TC39 started meeting regularly once again. As for ES4, there were plans for introducing a module system, classes, iterators, generators, destructuring, type annotations, proper tail calls, algebraic typing, and an assortment of other features. Due to how ambitious the project was, work on ES4 was repeatedly delayed. 32 | 33 | By 2007 the committee was split in two: ES3.1, which hailed a more incremental approach to ES3; and ES4, which was overdesigned and underspecified. It wouldn't be until August 2008pass:[Brendan Eich sent an email to the es-discuss mailing list in 2008 where he summarized the situation, almost 10 years after ES3 had been released.] when ES3.1 was agreed upon as the way forward, but later rebranded as ES5. Although ES4 would be abandoned, many of its features eventually made its way into ES6 (which was dubbed Harmony at the time of this resolution), while some of them still remain under consideration and a few others have been abandoned, rejected, or withdrawn. The ES3.1 update served as the foundation on top of which the ES4 specification could be laid in bits and pieces. 34 | 35 | In December 2009, on the 10-year anniversary since the publication of ES3, the fifth edition of ECMAScript was published. This edition codified de facto extensions to the language specification that had become common among browser implementations, adding +get+ and +set+ accessors, functional improvements to the `Array` prototype, reflection and introspection, as well as native support for JSON parsing and strict mode. 36 | 37 | A couple of years later, in June 2011, the specification was once again reviewed and edited to become the third edition of the international standard ISO/IEC 16262:2011, and formalized under ECMAScript 5.1. 38 | 39 | It took TC39 another four years to formalize ECMAScript 6, in June 2015. The sixth edition is the largest update to the language that made its way into publication, implementing many of the ES4 proposals that were deferred as part of the Harmony resolution. Throughout this book, we'll be exploring ES6 in depth. 40 | 41 | In parallel with the ES6 effort, in 2012 the ((("WHATWG")))WHATWG (a standards body interested in pushing the web forward) set out to document the differences between ES5.1 and browser implementations, in terms of compatibility and interoperability requirements. The task force standardized `String#substr`, which was previously unspecified; unified several methods for wrapping strings in HTML tags, which were inconsistent across browsers; and documented `Object.prototype` properties like `__proto__` and `__defineGetter__`, among other improvements.pass:[For the full set of changes made when merging the Web ECMAScript specification upstream, see the WHATWG blog.] This effort was condensed into a separate Web ECMAScript specification, which eventually made its way into Annex B in 2015. Annex B was an informative section of the core ECMAScript specification, meaning implementations weren't required to follow its suggestions. Jointly with this update, Annex B was also made normative and required for web browsers. 42 | 43 | The sixth edition is a significant milestone in the history of JavaScript. Besides the dozens of new features, ES6 marks a key inflection point where ECMAScript would become a ((("JavaScript", "standards evolution", startref="js1se")))((("standards evolution", startref="se1")))((("ECMAScript (ES)", "evolution of", startref="ec1eo")))rolling standard. 44 | 45 | [[ecmascript_as_a_rolling_standard]] 46 | === ECMAScript as a Rolling Standard 47 | 48 | Having ((("ECMAScript (ES)", "standardization of", id="ec1so")))spent 10 years without observing significant change to the language specification after ES3, and 4 years for ES6 to materialize, it was clear the TC39 process needed to improve. The revision process used to be deadline-driven. Any delay in arriving at consensus would cause long wait periods between revisions, which led to feature creep, causing more delays. Minor revisions were delayed by large additions to the specification, and large additions faced pressure to finalize so that the revision would be pushed through, avoiding further delays. 49 | 50 | Since ES6 came out, TC39 has streamlinedpass:[Check out the presentation "Post-ES6 Spec Process" from September 2013 that led to the streamlined proposal revisioning process here.] 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 (an HTML superset used to format ECMAScript specifications) and GitHub pull requests, greatly increasing the number of proposalspass:[Check out all of the proposals being considered by TC39.] being created as well as external participation by nonmembers. The new flow is continuous and thus, more transparent: while previously you'd have to download a Word doc or its PDF version from a web page, the latest draft of the https://mjavascript.com/out/spec-draft[specification] is now always available. 51 | 52 | Firefox, Chrome, Edge, Safari, and Node.js all offer over 95% compliance of the ES6 specification,pass:[Check out this detailed table reporting ES6 compatibility across browsers.] 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. 53 | 54 | The new process involves four different maturity stages.pass:[Take a look at the TC39 proposal process documentation.] The more mature a proposal is, the more likely it is to eventually make it into the specification. 55 | 56 | Any ((("ES6 maturity stages")))((("proposal stages")))discussion, idea, or proposal for a change or addition that has not yet been submitted as a formal proposal is considered to be an aspirational "strawman" proposal (stage 0), but only ((("TC39 standards committee", startref="tc391")))TC39 members can create strawman proposals. At the time of this writing, there are over a dozen active strawman proposals.pass:[ You can track strawman proposals.] 57 | 58 | At stage 1 a proposal is formalized and expected to address cross-cutting concerns, interactions with other proposals, and implementation concerns. Proposals at this stage should identify a discrete problem and offer a concrete solution to the problem. A stage 1 proposal often includes a high-level API description, illustrative usage examples, and a discussion of internal semantics and algorithms. Stage 1 proposals are likely to change significantly as they make their way through the process. 59 | 60 | Proposals in stage 2 offer an initial draft of the specification. At this point, it's reasonable to begin experimenting with actual implementations in runtimes. The implementation could come in the form of a polyfill, user code that mangles the runtime into adhering to the proposal; an engine implementation, natively providing support for the proposal; or compiled into something existing engines can execute, using build-time tools to transform source code. 61 | 62 | Proposals in stage 3 are candidate recommendations. In order for a proposal to advance to this stage, the specification editor and designated reviewers must have signed off on the final specification. Implementors should've expressed interest in the proposal as well. In practice, proposals move to this level with at least one browser implementation, a high-fidelity polyfill, or when supported by a build-time compiler like Babel. A stage 3 proposal is unlikely to change beyond fixes to issues identified in the wild. 63 | 64 | In order for a proposal to attain stage 4 status, two independent implementations need to pass acceptance tests. Proposals that make their way through to stage 4 will be included in the next revision of ECMAScript. 65 | 66 | New releases of the specification are expected to be published every year from now on. To accommodate the yearly release schedule, versions will now be referred to by their publication year. Thus ES6 becomes ES2015, then we have ES2016 instead of ES7, ES2017, and so on. Colloquially, ES2015 hasn't taken and is still largely regarded as ES6. ES2016 had been announced before the naming convention changed, thus it is sometimes still referred to as ES7. When we leave out ES6 due to its pervasiveness in the community, we end up with: ES6, ES2016, ES2017, ES2018, and so on. 67 | 68 | The streamlined proposal process combined with the yearly cut into standardization translates into a more consistent publication process, and it also means specification revision numbers are becoming less important. The focus is now on proposal stages, and we can expect references to specific revisions of the ECMAScript standard to ((("ECMAScript (ES)", "standardization of", startref="ec1so")))become more uncommon. 69 | 70 | === Browser Support and Complementary Tooling 71 | 72 | A stage 3 candidate recommendation proposal is most likely to make it into the specification in the next cut, provided two independent implementations land in JavaScript engines. Effectively, stage 3 proposals are considered safe to use in real-world applications, be it through an experimental engine implementation, a polyfill, or using a compiler. Stage 2 and earlier proposals are also used in the wild by JavaScript developers, tightening the feedback loop between implementors and consumers. 73 | 74 | Babel and similar ((("Babel", id="b1")))((("compilers")))compilers that take code as input and produce output native to the web platform (HTML, CSS, or JavaScript) are often referred to as ((("transpilers", seealso="Babel", id="t1")))_transpilers_, which are considered to be a subset of compilers. When we want to leverage a proposal that's not widely implemented in JavaScript engines in our code, compilers like Babel can transform the portions of code using that new proposal into something that's more widely supported by existing JavaScript implementations. 75 | 76 | This transformation can be done at build time, so that consumers receive code that's well supported by their JavaScript runtime of choice. This mechanism improves the runtime support baseline, giving JavaScript developers the ability to take advantage of new language features and syntax sooner. It is also significantly beneficial to specification writers and implementors, as it allows them to collect feedback regarding viability, desirability, and possible bugs or corner cases. 77 | 78 | A transpiler can take the ES6 source code we write and produce ES5 code that browsers can interpret more consistently. This is the most reliable way of running ES6 code in production today: using a build step to produce ES5 code that most old browsers, as well as modern browsers, can execute. 79 | 80 | The same applies to ES7 and beyond. As new versions of the language specification are released every year, we can expect compilers to support ES2017 input, ES2018 input, and so on. Similarly, as browser support becomes better, we can also expect compilers to reduce complexity in favor of ES6 output, then ES7 output, and so on. In this sense, we can think of JavaScript-to-JavaScript transpilers as a moving window that takes code written using the latest available language semantics and produces the most modern code they can output without compromising browser support. 81 | 82 | Let's talk about how you can use Babel as part of your workflow. 83 | 84 | ==== Introduction to the Babel Transpiler 85 | 86 | Babel can compile modern JavaScript code that relies on ES6 features into ES5. It produces human-readable code, making it more welcoming when we don't have a firm grasp on all of the new features we're using. 87 | 88 | The online https://mjavascript.com/out/babel-repl[Babel REPL (Read-Evaluate-Print Loop)] is an excellent way of jumping right into learning ES6, without any of the hassle of installing Node.js and the `babel` CLI, and manually compiling source code. 89 | 90 | The REPL provides us with a source code input area that gets automatically compiled in real time. We can see the compiled code to the right of our source code. 91 | 92 | Let's write some code into the REPL. You can use the following code snippet to get started: 93 | 94 | [source,javascript] 95 | ---- 96 | var double = value => value * 2 97 | console.log(double(3)) 98 | // <- 6 99 | ---- 100 | 101 | To the right of the source code we've entered, you'll see the transpiled ES5 equivalent, as shown in <>. As you update your source code, the transpiled result is also updated in real time. 102 | 103 | [[fig0101]] 104 | .The online Babel REPL in action—a great way to dive right into an interactive ES6 session 105 | image::images/pmjs_0101.png["Babel REPL"] 106 | 107 | The Babel REPL is an effective companion as a way of trying out some of the features introduced in this book. However, note that Babel doesn't transpile new built-ins, such as `Symbol`, `Proxy`, and `WeakMap`. Those references are instead left untouched, and it's up to the runtime executing the Babel output to provide those built-ins. If we want to support runtimes that haven't yet implemented these built-ins, we could import the `babel-polyfill` package in our code. 108 | 109 | In older versions of JavaScript, semantically correct implementations of these features are hard to accomplish or downright impossible. Polyfills may mitigate the problem, but they often can't cover all use cases and thus some compromises need to be made. We need to be careful and test our assumptions before we release transpiled code that relies on built-ins or polyfills into the wild. 110 | 111 | Given the situation, it might be best to wait until browsers support new built-ins holistically before we start using them. It is suggested that you consider alternative solutions that don't rely on built-ins. At the same time, it's important to learn about these features, as to not fall behind in our understanding of the JavaScript language. 112 | 113 | Modern browsers like Chrome, Firefox, and Edge now support a large portion of ES2015 and beyond, making their developer tools useful when we want to take the semantics of a particular feature for a spin, provided it's supported by the browser. When it comes to production-grade applications that rely on modern JavaScript features, a transpilation build-step is advisable so that your application supports a wider array of JavaScript runtimes. 114 | 115 | Besides the REPL, Babel offers a command-line tool written as a Node.js package. You can install it through `npm`, ((("npm")))the package manager for Node. 116 | 117 | [NOTE] 118 | ==== 119 | Download https://mjavascript.com/out/node[Node.js]. After ((("Node.js")))installing `node`, you'll also be able to use the `npm` command-line tool in your terminal. 120 | ==== 121 | 122 | Before getting started we'll make a project directory and a _package.json_ file, which is a manifest used to describe Node.js applications. We can create the _package.json_ file ((("package.json")))through the `npm` CLI: 123 | 124 | [source,shell] 125 | ---- 126 | mkdir babel-setup 127 | cd babel-setup 128 | npm init --yes 129 | ---- 130 | 131 | [NOTE] 132 | ==== 133 | Passing the `--yes` flag to the `init` command configures _package.json_ using the default values provided by `npm`, instead of asking us any questions. 134 | ==== 135 | 136 | Let's also create a file named _example.js_, containing the following bits of ES6 code. Save it to the _babel-setup_ directory you've just created, under a _src_ subdirectory: 137 | 138 | [source,javascript] 139 | ---- 140 | var double = value => value * 2 141 | console.log(double(3)) 142 | // <- 6 143 | ---- 144 | 145 | To install Babel, enter the following couple of commands into your favorite terminal: 146 | 147 | [source,shell] 148 | ---- 149 | npm install babel-cli​@6 --save-dev 150 | npm install babel-preset-env@1 --save-dev 151 | ---- 152 | 153 | [NOTE] 154 | ==== 155 | Packages installed by `npm` will be placed in a __node_modules__ directory at the project root. We can then access these packages by creating npm scripts or by using `require` statements in our application. 156 | 157 | Using the `--save-dev` flag will add these packages to our _package.json_ manifest as development dependencies, so that when copying our project to new environments we can reinstall every dependency just by running `npm install`. 158 | 159 | The `@` notation indicates we want to install a specific version of a package. Using `@6` we're telling `npm` to install the latest version of `babel-cli` in the `6.x` range. This preference is handy to future-proof our applications, as it would never install `7.0.0` or later versions, which might contain breaking changes that could not have been foreseen at the time of this writing. 160 | ==== 161 | 162 | For the next step, we'll replace the value of the `scripts` property in _package.json_ with the following. The `babel` command-line utility provided by `babel-cli` can take the entire contents of our _src_ directory, compile them into the desired output format, and save the results to a _dist_ directory, while preserving the original directory structure under a different root: 163 | 164 | [source,json] 165 | ---- 166 | { 167 | "scripts": { 168 | "build": "babel src --out-dir dist" 169 | } 170 | } 171 | ---- 172 | 173 | Together with the packages we've installed in the previous step, a minimal _package.json_ file could look like the code in the following snippet: 174 | 175 | [source,json] 176 | ---- 177 | { 178 | "scripts": { 179 | "build": "babel src --out-dir dist" 180 | }, 181 | "devDependencies": { 182 | "babel-cli": "^6.24.0", 183 | "babel-preset-env": "^1.2.1" 184 | } 185 | } 186 | ---- 187 | 188 | [NOTE] 189 | ==== 190 | Any commands enumerated in the `scripts` object can be executed through `npm run `, which temporarily modifies the `$PATH` environment variable so that we can run the command-line executables found in `babel-cli` without installing `babel-cli` globally on our system. 191 | ==== 192 | 193 | If you execute `npm run build` in your terminal now, you'll note that a _dist/example.js_ file is created. The output file will be identical to our original file, because Babel doesn't make assumptions, and we have to configure it first. Create a _.babelrc_ file next to _package.json_, and write the following JSON in it: 194 | 195 | [source,json] 196 | ---- 197 | { 198 | "presets": ["env"] 199 | } 200 | ---- 201 | 202 | The `env` preset, which we installed earlier via `npm`, adds a series of plugins to Babel that transform different bits of ES6 code into ES5. Among other things, this preset transforms arrow functions like the one in our _example.js_ file into ES5 code. The `env` Babel preset works by convention, enabling Babel transformation plugins according to feature support in the latest browsers. This preset is configurable, meaning we can decide how far back we want to cover browser support. The more browsers we support, the larger our transpiled bundle. The fewer browsers we support, the fewer customers we can satisfy. As always, research is of the essence to identify what the correct configuration for the Babel `env` preset is. By default, every transform is enabled, providing broad runtime support. 203 | 204 | Once we run our build script again, we'll observe that the output is now valid ES5 code: 205 | 206 | [source,shell] 207 | ---- 208 | » npm run build 209 | » cat dist/example.js 210 | "use strict" 211 | 212 | var double = function double(value) { 213 | return value * 2 214 | } 215 | console.log(double(3)) 216 | // <- 6 217 | ---- 218 | 219 | Let's jump into a different kind of tool, the `eslint` code linter, which can help us establish a code quality baseline for our ((("transpilers", seealso="Babel", startref="t1")))((("Babel", startref="b1")))applications. 220 | 221 | ==== Code Quality and Consistency with ESLint 222 | 223 | As ((("ESLint", id="esl1")))((("lint tools", id="lt1")))we develop a codebase we factor out snippets that are redundant or no longer useful, write new pieces of code, delete features that are no longer relevant or necessary, and shift chunks of code around while accommodating a new architecture. As the codebase grows, the team working on it changes as well: at first it may be a handful of people or even one person, but as the project grows in size so might the team. 224 | 225 | A lint tool can be used to identify syntax errors. Modern linters are often customizable, helping establish a ((("coding style conventions")))coding style convention that works for everyone on the team. By adhering to a consistent set of style rules and a quality baseline, we bring the team closer together in terms of coding style. Every team member has different opinions about coding styles, but those opinions can be condensed into style rules once we put a linter in place and agree upon a configuration. 226 | 227 | Beyond ensuring a program can be parsed, we might want to prevent `throw` statements throwing string literals as exceptions, or disallow `console.log` and `debugger` statements in production code. However, a rule demanding that every function call must have exactly one argument is probably too harsh. 228 | 229 | While linters are effective at defining and enforcing a coding style, we should be careful when devising a set of rules. If the lint step is too stringent, developers may become frustrated to the point where productivity is affected. If the lint step is too lenient, it may not yield a consistent coding style across our codebase. 230 | 231 | In order to strike the right balance, we may consider avoiding style rules that don't improve our programs in the majority of cases when they're applied. Whenever we're considering a new rule, we should ask ourselves whether it would noticeably improve our existing codebase, as well as new code going forward. 232 | 233 | ESLint is a modern linter that packs several plugins, sporting different rules, allowing us to pick and choose which ones we want to enforce. We decide whether failing to stick by these rules should result in a warning being printed as part of the output, or a halting error. To install `eslint`, we'll use `npm` ((("npm")))just like we did with `babel` in the previous section: 234 | 235 | [source,shell] 236 | ---- 237 | npm install eslint@4 --save-dev 238 | ---- 239 | 240 | Next, we need to configure ESLint. Since we installed `eslint` as a local dependency, we'll find its command-line tool in _node_modules/.bin_. Executing the following command will guide us through configuring ESLint for our project for the first time. To get started, indicate you want to use a popular style guide and choose Standard,footnoteref:[linters,Note that Standard is just a self-proclamation, and not actually standardized in any official capacity. It doesn't really matter which style guide you follow as long as you follow it consistently. Consistency helps reduce confusion while reading a project's codebase. The Airbnb style guide is also fairly popular and it doesn't omit semicolons by default, unlike Standard.] then pick JSON format for the configuration file: 241 | 242 | [source,shell] 243 | ---- 244 | ./node_modules/.bin/eslint --init 245 | ? How would you like to configure ESLint? 246 | Use a popular style guide 247 | ? Which style guide do you want to follow? Standard 248 | ? What format do you want your config file to be in? JSON 249 | ---- 250 | 251 | Besides individual rules, `eslint` allows us to extend predefined sets of rules, which are packaged up as Node.js modules. This is useful when sharing configuration across multiple projects, and even across a community. After picking Standard, we'll notice that ESLint adds a few dependencies to _package.json_, namely the packages that define the predefined Standard ruleset; and then creates a configuration file, named _.eslintrc.json_, with the following contents: 252 | 253 | [source,json] 254 | ---- 255 | { 256 | "extends": "standard", 257 | "plugins": [ 258 | "standard", 259 | "promise" 260 | ] 261 | } 262 | ---- 263 | 264 | Referencing the __node_modules/.bin__ directory, an implementation detail of how npm works, is far from ideal. While we used it when initializing our ESLint configuration, we shouldn't keep this reference around nor type it out whenever we lint our codebase. To solve this problem, we'll add the `lint` script in the next code snippet to our _package.json_: 265 | 266 | [source,json] 267 | ---- 268 | { 269 | "scripts": { 270 | "lint": "eslint ." 271 | } 272 | } 273 | ---- 274 | 275 | As you might recall from the Babel example, `npm run` adds __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. 276 | 277 | Let's consider the following _example.js_ file, which is purposely riddled with style issues, to demonstrate what ESLint does: 278 | 279 | [source,javascript] 280 | ---- 281 | var goodbye='Goodbye!' 282 | 283 | function hello(){ 284 | return goodbye} 285 | 286 | if(false){} 287 | ---- 288 | 289 | When we run the `lint` script, ESLint describes everything that's wrong with the file, as shown in <>. 290 | 291 | [[fig0102]] 292 | .The ESLint tool is a great way to keep your code free of syntax errors and, optionally, inconsistent coding style 293 | image::images/pmjs_0102.png["Validating a piece of source code through ESLint."] 294 | 295 | ESLint is able to fix most style problems automatically if we pass in a `--fix` flag. Add the following script to your _package.json_: 296 | 297 | 298 | [source,json] 299 | ---- 300 | { 301 | "scripts": { 302 | "lint-fix": "eslint . --fix" 303 | } 304 | } 305 | ---- 306 | 307 | 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 following bit of source code. The remaining errors weren't fixed because ESLint avoids making assumptions about our code, and prefers not to incur semantic changes. In doing so, `--fix` becomes a useful tool to resolve code style wrinkles without risking a broken program as a result. 308 | 309 | [role="pagebreak-before"] 310 | [source,javascript] 311 | ---- 312 | var goodbye = 'Goodbye!' 313 | 314 | function hello () { 315 | return goodbye 316 | } 317 | 318 | if (false) {} 319 | ---- 320 | 321 | [NOTE] 322 | ==== 323 | A similar kind of tool can be found in https://mjavascript.com/out/prettier[`prettier`], ((("prettier")))which can be used to automatically format your code. Prettier can be configured to automatically overwrite our code ensuring it follows preferences such as a given amount of spaces for indentation, single or double quotes, trailing commas, or a maximum line length. 324 | ==== 325 | 326 | Now that you know how to compile modern JavaScript into something every browser understands, and how to properly lint and format your code, let's jump into ES6 feature themes and the future of ((("ESLint", startref="esl1")))((("lint tools", startref="lt1")))JavaScript. 327 | 328 | === Feature Themes in ES6 329 | 330 | ES6 is big: ((("feature themes", id="ft1")))the language specification went from 258 pages in ES5.1 to over double that amount in ES6, at 566 pages. Each change to the specification falls in some of a few different categories: 331 | 332 | - Syntactic sugar 333 | - New mechanics 334 | - Better semantics 335 | - More built-ins and methods 336 | - Nonbreaking solutions to existing limitations 337 | 338 | Syntactic sugar ((("syntactic sugar")))is one of the most significant drivers in ES6. The new version offers a shorter way of expressing object inheritance, using the new class syntax; functions, using a shorthand syntax known as arrow functions; and properties, using property value shorthands. Several other features we'll explore, such as destructuring, rest, and spread, also offer semantically sound ways of writing programs. Chapters pass:[#es6-essentials] and pass:[#classes-symbols-objects-and-decorators] attack these aspects of ES6. 339 | 340 | We get several new mechanics to describe asynchronous code flows in ES6: _promises_, ((("promises")))((("iterators")))((("generators")))which represent the eventual result of an operation; _iterators_, which represent a sequence of values; and _generators_, a special kind of iterator that can produce a sequence of values. In ES2017, `async`/`await` ((("async/await")))builds on top of these new concepts and constructs, letting us write asynchronous routines that appear synchronous. We'll evaluate all of these iteration and flow control mechanisms in <>. 341 | 342 | 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 <>. 343 | 344 | Proxy objects ((("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")))polyfills exist, but they have limitations making them incompatible with the specification in some use cases. We'll devote <> to understanding proxies. 345 | 346 | Besides new built-ins, ES6 comes with several updates to `Number`, `Math`, `Array`, and ((("Number")))((("Math")))((("arrays")))((("strings")))strings. In <> we'll go over a plethora of new instance and static methods added to these built-ins. 347 | 348 | We are getting a new module system that's native to JavaScript. After going over the CommonJS module format that's used in Node.js, <> explains the semantics we can expect from native JavaScript modules. 349 | 350 | Due to the sheer amount of changes introduced by ES6, it's hard to reconcile its new features with our pre-existing knowledge of JavaScript. We'll spend all of <> analyzing the merits and importance of different individual features in ES6, so that you have a practical grounding upon which you can start experimenting with ((("feature themes", startref="ft1")))ES6 right away. 351 | 352 | === Future of JavaScript 353 | 354 | The JavaScript language ((("JavaScript", "future course of")))has evolved from its humble beginnings in 1995 to the formidable language it is today. While ES6 is a great step forward, it's not the finish line. Given we can expect new specification updates every year, it's important to learn how to stay up-to-date with the specification. 355 | 356 | Having gone over the rolling standard specification development process in <>, one of the best ways to keep up with the standard is by periodically visiting the TC39 proposals repository.pass:[Check out all of the proposals being considered by TC39.] Keep an eye on candidate recommendations (stage 3), which are likely to make their way into the specification. 357 | 358 | Describing an ever-evolving language in a book can be challenging, given the rolling nature of the standards process. An effective way of keeping up-to-date with the latest JavaScript updates is by watching the TC39 proposals repository, subscribing to weekly email newsletterspass:[There are many newsletters, including Pony Foo Weekly and JavaScript Weekly.], and reading JavaScript blogs.pass:[Many of the articles on Pony Foo and by Axel Rauschmayer focus on ECMAScript development.] 359 | 360 | At the time of this writing, the long awaited Async Functions proposal has made it into the specification and is slated for publication in ES2017. There are several candidates at the moment, such as dynamic `import()`, which enables asynchronous loading of native JavaScript modules, and a proposal to describe object property enumerations using the new rest and spread syntax that was first introduced for parameter lists and arrays in ES6. 361 | 362 | While the primary focus in this book is on ES6, we'll also learn about important candidate recommendations such as the aforementioned async functions, dynamic `import()` calls, or object rest/spread, among others. 363 | -------------------------------------------------------------------------------- /ch05.asciidoc: -------------------------------------------------------------------------------- 1 | [[leveraging-ecmascript-collections]] 2 | == Leveraging ECMAScript Collections 3 | 4 | JavaScript data structures ((("ECMAScript (ES)", id="ecmas5")))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. 5 | 6 | [source,javascript] 7 | ---- 8 | const registry = {} 9 | function set(name, meta) { 10 | registry[name] = meta 11 | } 12 | function get(name) { 13 | return registry[name] 14 | } 15 | set('contra', { description: 'Asynchronous flow control' }) 16 | set('dragula', { description: 'Drag and drop' }) 17 | set('woofmark', { description: 'Markdown and WYSIWYG editor' }) 18 | ---- 19 | 20 | There are several problems with this approach, outlined here: 21 | 22 | - Security issues where user-provided keys like `__proto__`, `toString`, or anything in `Object.prototype` break expectations and make interaction with this kind of hash-map data structures more cumbersome 23 | - When iterating using `for..in` we need to rely on `Object#hasOwnProperty` to make sure properties aren't inherited 24 | - Iteration over list items with `Object.keys(registry).forEach` is also verbose 25 | - Keys are limited to strings, making it hard to create hash-maps where you'd like to index values by DOM elements or other nonstring references 26 | 27 | The first problem could be fixed using a prefix, and being careful to always get or set values in the hash-map through functions that add those prefixes, to avoid mistakes. 28 | 29 | [source,javascript] 30 | ---- 31 | const registry = {} 32 | function set(name, meta) { 33 | registry['pkg:' + name] = meta 34 | } 35 | function get(name) { 36 | return registry['pkg:' + name] 37 | } 38 | ---- 39 | 40 | An alternative could also be using `Object.create(null)` instead of an empty object literal. In this case, the created object won't inherit from `Object.prototype`, meaning it won't be harmed by `__proto__` and friends. 41 | 42 | [source,javascript] 43 | ---- 44 | const registry = Object.create(null) 45 | function set(name, meta) { 46 | registry[name] = meta 47 | } 48 | function get(name) { 49 | return registry[name] 50 | } 51 | ---- 52 | 53 | For iteration we could create a `list` function that returns key/value tuples. 54 | 55 | [source,javascript] 56 | ---- 57 | const registry = Object.create(null) 58 | function list() { 59 | return Object.keys(registry).map(key => [key, registry[key]]) 60 | } 61 | ---- 62 | 63 | Or we could implement the iterator protocol on our hash-map. Here we are trading complexity in favor of convenience: the iterator code is more complicated to read than the former case where we had a `list` function with familiar `Object.keys` and `Array#map` methods. In the following example, however, accessing the list is even easier and more convenient than through `list`: following the iterator protocol means there's no need for a custom `list` function. 64 | 65 | [source,javascript] 66 | ---- 67 | const registry = Object.create(null) 68 | registry[Symbol.iterator] = () => { 69 | const keys = Object.keys(registry) 70 | return { 71 | next() { 72 | const done = keys.length === 0 73 | const key = keys.shift() 74 | const value = [key, registry[key]] 75 | return { done, value } 76 | } 77 | } 78 | } 79 | console.log([...registry]) 80 | ---- 81 | 82 | When it comes to using nonstring keys, though, we hit a hard limit in ES5 code. Luckily for us, though, ES6 collections provide us with an even better solution. ES6 collections don't have key-naming issues, and they facilitate collection behaviors, like the iterator we've implemented on our custom hash-map, out the box. At the same time, ES6 collections allow arbitrary keys, and aren't limited to string keys like regular JavaScript objects. 83 | 84 | Let's plunge into their practical usage and inner workings. 85 | 86 | === Using ES6 Maps 87 | 88 | ES6 ((("ECMAScript (ES)", "using ES6 maps", id="ecma5ues6m")))((("ES6 maps", id="es5m")))introduces built-in collections, such as `Map`, meant to alleviate implementation of patterns such as those we outlined earlier when building our own hash-map from scratch. `Map` ((("Map", seealso="ES6 maps")))is a key/value data structure in ES6 that more naturally and efficiently lends itself to creating maps in JavaScript without the need for object literals. 89 | 90 | ==== First Look into ES6 Maps 91 | 92 | Here's how what we had earlier would have looked when using ES6 maps. As you can see, the implementation details we've had to come up with for our custom ES5 hash-map are already built into `Map`, vastly simplifying our use case. 93 | 94 | [source,javascript] 95 | ---- 96 | const map = new Map() 97 | map.set('contra', { description: 'Asynchronous flow control' }) 98 | map.set('dragula', { description: 'Drag and drop' }) 99 | map.set('woofmark', { 100 | description: 'Markdown and WYSIWYG editor' 101 | }) 102 | console.log([...map]) 103 | ---- 104 | 105 | Once you have a map, ((("ES6 maps", "keys", id="es5k")))you can query whether it contains an entry by a `key` provided via the `map.has` ((("map.has")))method. 106 | 107 | [source,javascript] 108 | ---- 109 | map.has('contra') 110 | // <- true 111 | map.has('jquery') 112 | // <- false 113 | ---- 114 | 115 | Earlier, we pointed out that maps don't cast keys the way traditional objects do. This is typically an advantage, but you need to keep in mind that they won't be treated the same when querying the map, either. The following example uses the `Map` constructor, which takes an iterable of key/value pairs and then illustrates how maps don't cast their keys to strings. 116 | 117 | [source,javascript] 118 | ---- 119 | const map = new Map([[1, 'the number one']]) 120 | map.has(1) 121 | // <- true 122 | map.has('1') 123 | // <- false 124 | ---- 125 | 126 | The `map.get` ((("map.get")))method takes a map entry `key` and returns the `value` if an entry by the provided key is found. 127 | 128 | [source,javascript] 129 | ---- 130 | map.get('contra') 131 | // <- { description: 'Asynchronous flow control' } 132 | ---- 133 | 134 | Deleting values from the map is possible through ((("map.delete")))the `map.delete` method, providing the `key` for the entry you want to remove. 135 | 136 | [source,javascript] 137 | ---- 138 | map.delete('contra') 139 | map.get('contra') 140 | // <- undefined 141 | ---- 142 | 143 | You can clear the entries for a `Map` entirely, without losing the reference to the map itself. This can be handy in cases where you want to reset state for an object. 144 | 145 | [source,javascript] 146 | ---- 147 | const map = new Map([[1, 2], [3, 4], [5, 6]]) 148 | map.has(1) 149 | // <- true 150 | map.clear() 151 | map.has(1) 152 | // <- false 153 | [...map] 154 | // <- [] 155 | ---- 156 | 157 | Maps come with a read-only `.size` property that behaves similarly to ++Array#length++—at any point in time it gives you the current amount of entries in the map. 158 | 159 | [source,javascript] 160 | ---- 161 | const map = new Map([[1, 2], [3, 4], [5, 6]]) 162 | map.size 163 | // <- 3 164 | map.delete(3) 165 | map.size 166 | // <- 2 167 | map.clear() 168 | map.size 169 | // <- 0 170 | ---- 171 | 172 | You're able to use arbitrary objects when choosing map keys: you're not limited to using primitive values like symbols, numbers, or strings. Instead, you can use functions, objects, dates--and even DOM elements, too. Keys won't be cast to strings as we observe with plain JavaScript objects, but instead their references are preserved. 173 | 174 | [source,javascript] 175 | ---- 176 | const map = new Map() 177 | map.set(new Date(), function today() {}) 178 | map.set(() => 'key', { key: 'door' }) 179 | map.set(Symbol('items'), [1, 2]) 180 | ---- 181 | 182 | As an example, if we chose to use a symbol as the key for a map entry, we'd have to use a reference to that same symbol to get the item back, as demonstrated in the following snippet of code. 183 | 184 | [source,javascript] 185 | ---- 186 | const map = new Map() 187 | const key = Symbol('items') 188 | map.set(key, [1, 2]) 189 | map.get(Symbol('items')) // not the same reference as "key" 190 | // <- undefined 191 | map.get(key) 192 | // <- [1, 2] 193 | ---- 194 | 195 | Assuming an array of key/value pair `items` you want to include on a map, we could use a `for..of` ((("for..of")))loop to iterate over those `items` and add each pair to the map ((("map.set")))using `map.set`, as shown in the following code snippet. Note how we're using ((("destructuring")))destructuring during the `for..of` loop in order to effortlessly pull the `key` and `value` out of each two-dimensional item in `items`. 196 | 197 | [source,javascript] 198 | ---- 199 | const items = [ 200 | [new Date(), function today() {}], 201 | [() => 'key', { key: 'door' }], 202 | [Symbol('items'), [1, 2]] 203 | ] 204 | const map = new Map() 205 | for (const [key, value] of items) { 206 | map.set(key, value) 207 | } 208 | ---- 209 | 210 | Maps are ((("ES6 maps", "keys", startref="es5k")))iterable objects as well, because they implement a `Symbol.iterator` method. Thus, a copy of the map can be created using a `for..of` loop using similar code to what we've just used to create a map out of the `items` array. 211 | 212 | [source,javascript] 213 | ---- 214 | const copy = new Map() 215 | for (const [key, value] of map) { 216 | copy.set(key, value) 217 | } 218 | ---- 219 | 220 | In order to keep things simple, you can initialize maps directly using any object that follows the iterable protocol and produces a collection of `[key, value]` items. The following code snippet uses an array to seed a newly created `Map`. In this case, iteration occurs entirely in the `Map` constructor. 221 | 222 | [source,javascript] 223 | ---- 224 | const items = [ 225 | [new Date(), function today() {}], 226 | [() => 'key', { key: 'door' }], 227 | [Symbol('items'), [1, 2]] 228 | ] 229 | const map = new Map(items) 230 | ---- 231 | 232 | Creating a copy of a map is even easier: you feed the map you want to copy into a new map's constructor, and get a copy back. There isn't a special `new Map(Map)` overload. Instead, we take advantage that `map` implements the iterable protocol and also consumes iterables when constructing a new map. The following code snippet demonstrates how simple that is. 233 | 234 | [source,javascript] 235 | ---- 236 | const copy = new Map(map) 237 | ---- 238 | 239 | Just like maps are easily fed into other maps because they're iterable objects, they're also easy to consume. The following piece of code demonstrates how we can use the ((("spread operator")))spread operator to this effect. 240 | 241 | [source,javascript] 242 | ---- 243 | const map = new Map() 244 | map.set(1, 'one') 245 | map.set(2, 'two') 246 | map.set(3, 'three') 247 | console.log([...map]) 248 | // <- [[1, 'one'], [2, 'two'], [3, 'three']] 249 | ---- 250 | 251 | In the following piece of code we've combined several new features in ES6: `Map`, the `for..of` loop, `let` variables, and a template literal. 252 | 253 | [source,javascript] 254 | ---- 255 | const map = new Map() 256 | map.set(1, 'one') 257 | map.set(2, 'two') 258 | map.set(3, 'three') 259 | for (const [key, value] of map) { 260 | console.log(`${ key }: ${ value }`) 261 | // <- '1: one' 262 | // <- '2: two' 263 | // <- '3: three' 264 | } 265 | ---- 266 | 267 | Even though map items are accessed through a programmatic API, their keys are unique, just like with hash-maps. Setting a key over and over again will only overwrite its value. The following code snippet demonstrates how writing the `'a'` item over and over again results in a map containing only a single item. 268 | 269 | [source,javascript] 270 | ---- 271 | const map = new Map() 272 | map.set('a', 1) 273 | map.set('a', 2) 274 | map.set('a', 3) 275 | console.log([...map]) 276 | // <- [['a', 3]] 277 | ---- 278 | 279 | ES6 maps compare ((("ES6 maps", "keys")))keys using an algorithm called `SameValueZero` in ((("SameValueZero")))the specification, where `NaN` equals `NaN` but `-0` equals `+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. 280 | 281 | [source,javascript] 282 | ---- 283 | console.log(NaN === NaN) 284 | // <- false 285 | console.log(-0 === +0) 286 | // <- true 287 | const map = new Map() 288 | map.set(NaN, 'one') 289 | map.set(NaN, 'two') 290 | map.set(-0, 'three') 291 | map.set(+0, 'four') 292 | console.log([...map]) 293 | // <- [[NaN, 'two'], [0, 'four']] 294 | ---- 295 | 296 | When you ((("iteration protocol", "in maps")))iterate over a `Map`, you are actually looping over its `.entries()`. That means that you don't need to explicitly iterate over `.entries()`. It'll be done on your behalf anyway: `map[Symbol.iterator]` points to `map.entries`. The `.entries()` method returns an iterator for the key/value pairs in the map. 297 | 298 | [source,javascript] 299 | ---- 300 | console.log(map[Symbol.iterator] === map.entries) 301 | // <- true 302 | ---- 303 | 304 | There are two other `Map` iterators you can leverage: `.keys()` (((".keys()", primary-sortas="keys")))(((".keys()")))and `.values()`. (((".values()", primary-sortas="values")))(((".values()")))The first enumerates keys in a map while the second enumerates values, as opposed to `.entries()`, which enumerates key/value pairs. The following snippet illustrates the differences between all three methods. 305 | 306 | [source,javascript] 307 | ---- 308 | const map = new Map([[1, 2], [3, 4], [5, 6]]) 309 | console.log([...map.keys()]) 310 | // <- [1, 3, 5] 311 | console.log([...map.values()]) 312 | // <- [2, 4, 6] 313 | console.log([...map.entries()]) 314 | // <- [[1, 2], [3, 4], [5, 6]] 315 | ---- 316 | 317 | 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. 318 | 319 | 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 cast into strings in the case of `Map`, as demonstrated here. 320 | 321 | [source,javascript] 322 | ---- 323 | const map = new Map([ 324 | [NaN, 1], 325 | [Symbol(), 2], 326 | ['key', 'value'], 327 | [{ name: 'Kent' }, 'is a person'] 328 | ]) 329 | map.forEach((value, key) => console.log(key, value)) 330 | // <- NaN 1 331 | // <- Symbol() 2 332 | // <- 'key' 'value' 333 | // <- { name: 'Kent' } 'is a person' 334 | ---- 335 | 336 | Earlier, we brought up the ability of providing arbitrary object references as the key to a `Map` entry. Let's go into a concrete use case for that ((("ECMAScript (ES)", "using ES6 maps", startref="ecma5ues6m")))((("ES6 maps", startref="es5m")))API. 337 | 338 | ==== Hash-Maps and the DOM 339 | 340 | In ES5, ((("ES6 maps", "hash maps and the DOM", id="es5hmatdom")))((("DOM elements", "in maps", secondary-sortas="maps", id="dom5im")))whenever we wanted to associate a DOM element with an API object connecting that element with some library, we had to implement a verbose and slow pattern such as the one in the following code listing. That code returns an API object with a few methods associated to a given DOM element, allowing us to put DOM elements on a map from which we can later retrieve the API object for a DOM element. 341 | 342 | [source,javascript] 343 | ---- 344 | const map = [] 345 | function customThing(el) { 346 | const mapped = findByElement(el) 347 | if (mapped) { 348 | return mapped 349 | } 350 | const api = { 351 | // custom thing api methods 352 | } 353 | const entry = storeInMap(el, api) 354 | api.destroy = destroy.bind(null, entry) 355 | return api 356 | } 357 | function storeInMap(el, api) { 358 | const entry = { el, api } 359 | map.push(entry) 360 | return entry 361 | } 362 | function findByElement(query) { 363 | for (const { el, api } of map) { 364 | if (el === query) { 365 | return api 366 | } 367 | } 368 | } 369 | function destroy(entry) { 370 | const index = map.indexOf(entry) 371 | map.splice(index, 1) 372 | } 373 | ---- 374 | 375 | 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. 376 | 377 | The following example shows how `Map` would reduce the burden of maintenance in user code. 378 | 379 | [source,javascript] 380 | ---- 381 | const map = new Map() 382 | function customThing(el) { 383 | const mapped = findByElement(el) 384 | if (mapped) { 385 | return mapped 386 | } 387 | const api = { 388 | // custom thing api methods 389 | destroy: destroy.bind(null, el) 390 | } 391 | storeInMap(el, api) 392 | return api 393 | } 394 | function storeInMap(el, api) { 395 | map.set(el, api) 396 | } 397 | function findByElement(el) { 398 | return map.get(el) 399 | } 400 | function destroy(el) { 401 | map.delete(el) 402 | } 403 | ---- 404 | 405 | 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. 406 | 407 | [source,javascript] 408 | ---- 409 | const map = new Map() 410 | function customThing(el) { 411 | const mapped = map.get(el) 412 | if (mapped) { 413 | return mapped 414 | } 415 | const api = { 416 | // custom thing api methods 417 | destroy: () => map.delete(el) 418 | } 419 | map.set(el, api) 420 | return api 421 | } 422 | ---- 423 | 424 | Maps aren't ((("ES6 maps", "hash maps and the DOM", startref="es5hmatdom")))((("DOM elements", "in maps", secondary-sortas="maps", startref="dom5im")))the only kind of built-in collection in ES6; there's also `WeakMap`, `Set`, and `WeakSet`. Let's proceed by digging into `WeakMap`. 425 | 426 | === Understanding and Using WeakMap 427 | 428 | For ((("WeakMap", id="wm5")))((("ES6 maps", "WeakMap", id="es5wm")))the most part, you can think of `WeakMap` as a subset of `Map`. The `WeakMap` collection has a reduced API surface with fewer affordances than what we could find in `Map`. Collections created using `WeakMap` are not iterable like `Map`, meaning there is no iterable protocol in `WeakMap`, no `WeakMap#entries`, no `WeakMap#keys`, no `WeakMap#values`, no `WeakMap#forEach`, and no `WeakMap#clear` methods. 429 | 430 | Another distinction found in `WeakMap` is that every `key` must be an object. This is in contrast with `Map`, where, while object references were allowed as keys, they weren't enforced. Remember that `Symbol` is a value type, and as such, isn't allowed either. 431 | 432 | [source,javascript] 433 | ---- 434 | const map = new WeakMap() 435 | map.set(Date.now, 'now') 436 | map.set(1, 1) 437 | // <- TypeError 438 | map.set(Symbol(), 2) 439 | // <- TypeError 440 | ---- 441 | 442 | 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 associated metadata. You can now keep that metadata in a `WeakMap` using `person` as the key. 443 | 444 | In that sense, a `WeakMap` is most useful when the component maintaining it doesn't own the mapped objects, but wants to assign its own information to them without modifying the original objects or their lifecycle; letting memory be reclaimed when, for example, a DOM node is removed from the document. 445 | 446 | To initialize a `WeakMap`, you are able to provide an iterable through the constructor. This should be a list of key/value pairs, just like with `Map`. 447 | 448 | [source,javascript] 449 | ---- 450 | const map = new WeakMap([ 451 | [new Date(), 'foo'], 452 | [() => 'bar', 'baz'] 453 | ]) 454 | ---- 455 | 456 | While `WeakMap` has a smaller API surface in order to effectively allow for weak references, it still carries `.has`, `.get`, and `.delete` methods like `Map` does. The brief snippet of code shown next demonstrates these methods. 457 | 458 | [source,javascript] 459 | ---- 460 | const date = new Date() 461 | const map = new WeakMap([[date, 'foo'], [() => 'bar', 'baz']]) 462 | map.has(date) 463 | // <- true 464 | map.get(date) 465 | // <- 'foo' 466 | map.delete(date) 467 | map.has(date) 468 | // <- false 469 | ---- 470 | 471 | ==== Is WeakMap a Worse Map? 472 | 473 | The distinction that makes `WeakMap` worth the trouble is in its name. Given that `WeakMap` holds references to its keys weakly, those objects 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. 474 | 475 | 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. 476 | 477 | 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`. 478 | 479 | In so many words, then: no, `WeakMap` is definitely not worse than ++Map++—they just cater to different ((("WeakMap", startref="wm5")))((("ES6 maps", "WeakMap", startref="es5wm")))use cases. 480 | 481 | === Sets in ES6 482 | 483 | The `Set` ((("ES6 sets", id="ess5")))((("Set", id="set5")))built-in is a new collection type in ES6 used to represent a grouping of values. In several aspects, `Set` is similar to `Map`: 484 | 485 | - `Set` is also iterable 486 | - `Set` constructor also accepts an iterable 487 | - `Set` also has a `.size` property 488 | - `Set` values can be arbitrary values or object references, like `Map` keys 489 | - `Set` values must be unique, like `Map` keys 490 | - `NaN` equals `NaN` when it comes to `Set` too 491 | - All of `.keys`, `.values`, `.entries`, `.forEach`, `.has`, `.delete`, and `.clear` 492 | 493 | 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. 494 | 495 | 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. 496 | 497 | 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. 498 | 499 | [source,javascript] 500 | ---- 501 | const set = new Set() 502 | set.add({ an: 'example' }) 503 | ---- 504 | 505 | 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. 506 | 507 | [source,javascript] 508 | ---- 509 | const set = new Set(['a', 'b', 'c']) 510 | console.log([...set]) 511 | // <- ['a', 'b', 'c'] 512 | ---- 513 | 514 | In the following example you can note how a set won't contain duplicate entries: every element in a `Set` must be unique. 515 | 516 | [source,javascript] 517 | ---- 518 | const set = new Set(['a', 'b', 'b', 'c', 'c']) 519 | console.log([...set]) 520 | // <- ['a', 'b', 'c'] 521 | ---- 522 | 523 | 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. 524 | 525 | [source,javascript] 526 | ---- 527 | function divs() { 528 | return document.querySelectorAll('div') 529 | } 530 | const set = new Set(divs()) 531 | console.log(set.size) 532 | // <- 56 533 | divs().forEach(div => set.add(div)) 534 | console.log(set.size) 535 | // <- 56 536 | ---- 537 | 538 | Given that a `Set` has no keys, the `Set#entries` method returns an iterator of `[value, value]` for each element in the set. 539 | 540 | [source,javascript] 541 | ---- 542 | const set = new Set(['a', 'b', 'c']) 543 | console.log([...set.entries()]) 544 | // <- [['a', 'a'], ['b', 'b'], ['c', 'c']] 545 | ---- 546 | 547 | The `Set#entries` method ((("Set#entries")))is consistent with `Map#entries`, which ((("Map#entries")))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. 548 | 549 | As demonstrated next, the default `Set` iterator ((("Set#values")))uses `Set#values`, as opposed to `Map`, which defined its iterator as `Map#entries`. 550 | 551 | [source,javascript] 552 | ---- 553 | const map = new Map() 554 | console.log(map[Symbol.iterator] === map.entries) 555 | // <- true 556 | const set = new Set() 557 | console.log(set[Symbol.iterator] === set.entries) 558 | // <- false 559 | console.log(set[Symbol.iterator] === set.values) 560 | // <- true 561 | ---- 562 | 563 | The `Set#keys` method ((("Set#keys")))also returns an iterator for values, again for consistency, and it's in fact a reference to the `Set#values` iterator. 564 | 565 | [source,javascript] 566 | ---- 567 | const set = new Set() 568 | console.log(set.keys === set.values) 569 | // <- true 570 | ---- 571 | 572 | === ES6 WeakSets 573 | 574 | In a ((("WeakSet", id="ws5")))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. 575 | 576 | You can only `.set`, `.delete`, and check if the `WeakSet` `.has` a given `value`. Just like in `Set`, there's no `.get` because sets are one-dimensional. 577 | 578 | Like with `WeakMap`, we aren't allowed to add primitive values such as strings or symbols to a `WeakSet`. 579 | 580 | [source,javascript] 581 | ---- 582 | const set = new WeakSet() 583 | set.add('a') 584 | // <- TypeError 585 | set.add(Symbol()) 586 | // <- TypeError 587 | ---- 588 | 589 | Passing iterators to the constructor is allowed, even though a `WeakSet` instance is not iterable itself. That iterable will be iterated when the set is constructed, adding each entry in the iterable sequence to the set. The following snippet of code serves as an example. 590 | 591 | [source,javascript] 592 | ---- 593 | const set = new WeakSet([ 594 | new Date(), 595 | {}, 596 | () => {}, 597 | [1] 598 | ]) 599 | ---- 600 | 601 | As a use case for `WeakSet`, you may consider the following piece of code where we have a `Car` class that ensures its methods are only called upon car objects that are instances of the `Car` class by using a `WeakSet`. 602 | 603 | [source,javascript] 604 | ---- 605 | const cars = new WeakSet() 606 | class Car { 607 | constructor() { 608 | cars.add(this) 609 | } 610 | fuelUp() { 611 | if (!cars.has(this)) { 612 | throw new TypeError('Car#fuelUp called on a non-Car!') 613 | } 614 | } 615 | } 616 | ---- 617 | 618 | For a better use case, consider the following `listOwnProperties` interface, where the provided object is recursively iterated in order to print every property of a tree. The `listOwnProperties` function should also know how to handle circular references, instead of becoming stuck in an infinite loop. How would you implement such an API? 619 | 620 | [source,javascript] 621 | ---- 622 | const circle = { cx: 20, cy: 5, r: 15 } 623 | circle.self = circle 624 | listOwnProperties({ 625 | circle, 626 | numbers: [1, 5, 7], 627 | sum: (a, b) => a + b 628 | }) 629 | // <- circle.cx: 20 630 | // <- circle.cy: 5 631 | // <- circle.r: 15 632 | // <- circle.self: [circular] 633 | // <- numbers.0: 1 634 | // <- numbers.1: 5 635 | // <- numbers.2: 7 636 | // <- sum: (a, b) => a + b 637 | ---- 638 | 639 | One way to do it would be by keeping a list of `seen` references in a `WeakSet`, so that we don't need to worry about nonlinear lookups. We use a `WeakSet` instead of a `Set` because we don't need any of the extra features that can be found in a `Set`. 640 | 641 | [source,javascript] 642 | ---- 643 | function listOwnProperties(input) { 644 | recurse(input) 645 | 646 | function recurse(source, lastPrefix, seen = new WeakSet()) { 647 | Object.keys(source).forEach(printOrRecurse) 648 | 649 | function printOrRecurse(key) { 650 | const value = source[key] 651 | const prefix = lastPrefix 652 | ? `${ lastPrefix }.${ key }` 653 | : key 654 | const shouldRecur = ( 655 | isObject(value) || 656 | Array.isArray(value) 657 | ) 658 | if (shouldRecur) { 659 | if (!seen.has(value)) { 660 | seen.add(value) 661 | recurse(value, prefix, seen) 662 | } else { 663 | console.log(`${ prefix }: [circular]`) 664 | } 665 | } else { 666 | console.log(`${ prefix }: ${ value }`) 667 | } 668 | } 669 | } 670 | } 671 | function isObject(value) { 672 | return Object.prototype.toString.call(value) === 673 | '[object Object]' 674 | } 675 | ---- 676 | 677 | A far more common use case would be to keep a list of DOM elements. Consider the case of a DOM library that needs to manipulate DOM elements in some way the first time it interacts with them, but which also can't leave any traces behind. Perhaps the library wants to add children onto the `target` element but it has no surefire way of identifying those children, and it doesn't want to meddle with the `target` either. Or maybe it wants to do something contextual, but only the first time it's called. 678 | 679 | [source,javascript] 680 | ---- 681 | const elements = new WeakSet() 682 | function dommy(target) { 683 | if (elements.has(target)) { 684 | return 685 | } 686 | elements.add(target) 687 | // do work .. 688 | }) 689 | ---- 690 | 691 | Whatever the reason, whenever we want to keep flags associated with a DOM element without visibly altering that DOM element, `WeakSet` is probably the way to go. If instead you wanted to associate arbitrary data instead of a simple flag, then maybe you should use `WeakMap`. When it comes to deciding whether to use `Map`, `WeakMap`, `Set`, or `WeakSet`, there's a series of questions you should ask yourself. For instance, if you need to keep object-related data, then you should know to look at weak collections. If your only concern is whether something is present, then you probably need a `Set`. If you are looking to create a cache, you should probably ((("WeakSet", startref="ws5")))use a `Map`. 692 | 693 | Collections in ES6 provide built-in solutions for common use cases that were previously cumbersome to implement by users, such as the case of `Map`, or hard to execute correctly, as in the case of `WeakMap`, where we allow references to be released if they're no longer interesting, avoiding memory ((("ES6 sets", startref="ess5")))((("Set", startref="set5")))((("ECMAScript (ES)", startref="ecmas5")))leaks. 694 | -------------------------------------------------------------------------------- /ch08.asciidoc: -------------------------------------------------------------------------------- 1 | [[javascript-modules]] 2 | == JavaScript Modules 3 | 4 | Over the years, we've seen multiple different ways in which to split code into more manageable units. For the longest time we've had the module pattern, where you simply wrapped pieces of code in self-invoking function expressions. You had to be careful to sort your scripts so that each script came after all of its dependencies. 5 | 6 | A while later, the ((("RequireJS")))RequireJS library was born. It provided a way of defining the dependencies of each module programmatically, so that a dependency graph is created and you wouldn't have to worry about sorting your scripts anymore. RequireJS demands that you provide an array of strings used to identify your dependencies and also wrap modules in a function call, which would then receive those dependencies as parameters. Many other libraries provide similar functionality but offer a slightly different API. 7 | 8 | 9 | Other complexity management mechanisms exist, such as the dependency injection mechanism in ((("AngularJS")))AngularJS, where you define named components using functions where you can, in turn, specify other named component dependencies. AngularJS carries the load of dependency injection on your behalf, so you only have to name components and specify dependencies. 10 | 11 | CommonJS (CJS) surfaced as an alternative to RequireJS, and it was swiftly popularized by Node.js soon afterwards. In this chapter we'll take a look at CommonJS, which is still heavily in use today. We'll then cover the module system introduced to native JavaScript in ES6, and lastly we'll explore interoperability between CommonJS and native JavaScript modules--also known as ECMAScript modules (ESM). 12 | 13 | === CommonJS 14 | 15 | Unlike ((("CommonJS", id="cjs8")))other module formats where modules are declared programmatically, in CommonJS every file is a module. CommonJS modules have an implicit local scope, while the `global` scope needs to be accessed explicitly. CommonJS modules can dynamically export a public interface consumers can interact with. CommonJS modules import their dependencies dynamically as well, resolving dependencies through `require` function calls. These `require` function calls are synchronous and return the interface exposed by required modules. 16 | 17 | Interpreting the definition of a module format without looking at some code can be confusing. The following code snippet shows what a reusable CommonJS module file may look like. Both the `has` and `union` functions are local to our module's scope. Given that we've assigned `union` to `module.exports`, that'll be the public API for our module. 18 | 19 | [source,javascript] 20 | ---- 21 | function has(list, item) { 22 | return list.includes(item) 23 | } 24 | function union(list, item) { 25 | if (has(list, item)) { 26 | return list 27 | } 28 | return [...list, item] 29 | } 30 | module.exports = union 31 | ---- 32 | 33 | Suppose we take that snippet of code and save it as _union.js_. We can now consume _union.js_ in another CommonJS module. Let's call that one _app.js_. In order to consume _union.js_, we call `require` passing in a relative path to the _union.js_ file. 34 | 35 | [source,javascript] 36 | ---- 37 | const union = require('./union.js') 38 | console.log(union([1, 2], 3)) 39 | // <- [1, 2, 3] 40 | console.log(union([1, 2], 2)) 41 | // <- [1, 2] 42 | ---- 43 | 44 | [WARNING] 45 | ==== 46 | We can omit the ((("CommonJS", "file extension use")))file extension as long as it's _.js_ or _.json_, but this is discouraged. 47 | 48 | While the file extension is optional for `require` statements and when using the `node` CLI, we should strongly consider getting into the habit of including it nevertheless. https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-module-system[Browser implementations of ESM] won't have this luxury, since that'd entail extra roundtrips to figure out the correct endpoint for a JavaScript module HTTP resource. 49 | ==== 50 | 51 | We could run `_app.js_` in its current state through the CLI for Node.js, `node`, as seen in the next snippet. 52 | 53 | [source,shell] 54 | ---- 55 | » node app.js 56 | # [1, 2, 3] 57 | # [1, 2] 58 | ---- 59 | 60 | 61 | 62 | [NOTE] 63 | ==== 64 | After installing https://mjavascript.com/out/node[Node.js], you'll be able to use the `node` program in your terminal.((("Node.js"))) 65 | ==== 66 | 67 | The `require` function ((("require")))in CJS can be treated dynamically, just like any other JavaScript function. This aspect of `require` is sometimes leveraged to dynamically `require` different modules that conform to one interface. As an example, let's conjure up a _templates_ directory with a number of view template functions. Our templates will take a model and return an HTML string. 68 | 69 | The template found in the following code snippet renders an item of a grocery shopping list by reading its attributes from a `model` object. 70 | 71 | [source,javascript] 72 | ---- 73 | // views/item.js 74 | module.exports = model => `
  • 75 | ${ model.amount } 76 | x 77 | ${ model.name } 78 |
  • ` 79 | ---- 80 | 81 | Our application could print a `
  • ` by leveraging the _item.js_ view template. 82 | 83 | [source,javascript] 84 | ---- 85 | // app.js 86 | const renderItem = require('./views/item.js') 87 | const html = renderItem({ 88 | name: 'Banana bread', 89 | amount: 3 90 | }) 91 | console.log(html) 92 | ---- 93 | 94 | <> shows our tiny application in action. 95 | 96 | [[fig8_1]] 97 | .Rendering a model as HTML is as easy as saying template literal expression interpolation! 98 | image::images/pmjs_0801.png["Printing an item for our grocery shopping list"] 99 | 100 | The next template we'll make renders the grocery list itself. It receives an array of items, and renders each of them by reusing the _item.js_ template from the previous code snippet. 101 | 102 | [source,javascript] 103 | ---- 104 | // views/list.js 105 | const renderItem = require('./item.js') 106 | 107 | module.exports = model => `
      108 | ${ model.map(renderItem).join('\n') } 109 |
    ` 110 | ---- 111 | 112 | We can consume the _list.js_ template in a very similar way to what we did before, but we'll need to adjust the model passed into the template so that we provide a collection of items instead of a single one. 113 | 114 | [source,javascript] 115 | ---- 116 | // app.js 117 | const renderList = require('./views/list.js') 118 | const html = renderList([{ 119 | name: 'Banana bread', 120 | amount: 3 121 | }, { 122 | name: 'Chocolate chip muffin', 123 | amount: 2 124 | }]) 125 | console.log(html) 126 | ---- 127 | 128 | <> shows our updated application in all its glory. 129 | 130 | [[Fig8_2]] 131 | .Composing components made with template literals can be as simple as we choose to make them 132 | image::images/pmjs_0802.png["Printing a grocery shopping list"] 133 | 134 | In the examples so far, we've written short modules that are only concerned with producing an HTML view after matching a `model` object with the corresponding view template. A simple API encourages reusability, which is why we're easily able to render the items for a list by mapping their models to the _item.js_ templating function, and joining their HTML representations with newlines. 135 | 136 | Given that the views all have a similar API where they take a model and return an HTML string, we can treat them uniformly. If we wanted a `render` ((("render")))function that could render any template, we could easily do that, thanks to the dynamic nature of `require`. The next example shows how we can construct the path to a template module. An important distinction is how `require` calls don't necessarily need to be on the top level of a module. Calls to `require` ((("require")))can be anywhere, even embedded within other functions. 137 | 138 | [source,javascript] 139 | ---- 140 | // render.js 141 | module.exports = function render(template, model) { 142 | return require(`./views/${ template }`.js)(model) 143 | } 144 | ---- 145 | 146 | Once we had such an API, we wouldn't have to worry about carefully constructing `require` statements that match the directory structure of our view templates, because the _render.js_ module could take care of that. Rendering any template becomes a matter of calling the `render` function with the template's name and the model for that template, as demonstrated in the following code and <>. 147 | 148 | [source,javascript] 149 | ---- 150 | // app.js 151 | const render = require('./render.js') 152 | console.log(render('item', { 153 | name: 'Banana bread', 154 | amount: 1 155 | })) 156 | console.log(render('list', [{ 157 | name: 'Apple pie', 158 | amount: 2 159 | }, { 160 | name: 'Roasted almond', 161 | amount: 25 162 | }])) 163 | ---- 164 | 165 | [[fig8-3]] 166 | .Creating a bare bones HTML rendering application is made easy by template literals 167 | image::images/pmjs_0803.png["Printing different views through a normalized render function."] 168 | 169 | 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 ((("CommonJS", startref="cjs8")))with CJS. 170 | 171 | === JavaScript Modules 172 | 173 | As ((("ES6 modules", id="esm8")))we explored the CommonJS module system, you might've noticed how the API is simple but powerful and flexible. ES6 modules offer an even simpler API that's almost as powerful at the expense of some flexibility. 174 | 175 | ==== Strict Mode 176 | 177 | In the ((("ES6 modules", "strict mode", id="es8sm")))((("strict mode", id="sm8")))ES6 module system, strict mode is turned on by default. Strict mode is a featurepass:[Read this comprehensive article about strict mode on Mozilla's MDN.] that disallows bad parts of the language, and turns some silent errors into loud exceptions being thrown. Taking into account these disallowed features, compilers can enable optimizations, making JavaScript runtime faster and safer. 178 | 179 | - Variables must be declared 180 | - Function parameters must have unique names 181 | - Using `with` statements is forbidden 182 | - Assignment to read-only properties results in errors being thrown 183 | - Octal numbers like `00740` are syntax errors 184 | - Attempts to `delete` undeletable properties throw an error 185 | - `delete prop` is a syntax error, instead of assuming `delete global.prop` 186 | - `eval` doesn't introduce new variables into its surrounding scope 187 | - `eval` and `arguments` can't be bound or assigned to 188 | - `arguments` doesn't magically track changes to method parameters 189 | - `arguments.callee` is no longer supported, throws a `TypeError` 190 | - `arguments.caller` is no longer supported, throws a `TypeError` 191 | - Context passed as `this` in method invocations is not "boxed" into an `Object` 192 | - No longer able to use `fn.caller` and `fn.arguments` to access the JavaScript stack 193 | - Reserved words (e.g., `protected`, `static`, `interface`, etc.) cannot be ((("ES6 modules", "strict mode", startref="es8sm")))((("strict mode", startref="sm8")))bound 194 | 195 | Let's now dive into the `export` statement. 196 | 197 | ==== export Statements 198 | 199 | In ((("CommonJS")))CommonJS ((("ES6 modules", "export statements", id="esm8es")))((("export statements", id="exs8")))modules, you export values by exposing them on `module.exports`. You can expose anything from a value type to an object, an array, or a function, as seen in the next few code snippets. 200 | 201 | 202 | [source,javascript] 203 | ---- 204 | module.exports = 'hello' 205 | ---- 206 | 207 | [source,javascript] 208 | ---- 209 | module.exports = { hello: 'world' } 210 | ---- 211 | 212 | [source,javascript] 213 | ---- 214 | module.exports = ['hello', 'world'] 215 | ---- 216 | 217 | [source,javascript] 218 | ---- 219 | module.exports = function hello() {} 220 | ---- 221 | 222 | ES6 modules are files that may expose an API through `export` statements. Declarations in ESM are scoped to the local module, just like we observed about CommonJS. Any variables declared inside a module aren't available to other modules unless they're explicitly exported as part of that module's API and then imported in the module that wants to access them. 223 | 224 | ===== Exporting a default binding 225 | 226 | You can mimic the ((("export statements", "default")))CommonJS code we just saw by replacing `module.exports =` with `export default` statements. 227 | 228 | 229 | [source,javascript] 230 | ---- 231 | export default 'hello' 232 | ---- 233 | 234 | [source,javascript] 235 | ---- 236 | export default { hello: 'world' } 237 | ---- 238 | 239 | [source,javascript] 240 | ---- 241 | export default ['hello', 'world'] 242 | ---- 243 | 244 | [source,javascript] 245 | ---- 246 | export default function hello() {} 247 | ---- 248 | 249 | In CommonJS, `module.exports` can be assigned-to dynamically. 250 | 251 | [source,javascript] 252 | ---- 253 | function initialize() { 254 | module.exports = 'hello!' 255 | } 256 | initialize() 257 | ---- 258 | 259 | In contrast with CJS, `export` statements in ESM can only be placed at the top level. "Top-level only" `export` statements is a good constraint to have, as there aren't many good reasons to dynamically define and expose an API based on method calls. This limitation also helps compilers and static analysis tools parse ES6 modules. 260 | 261 | [source,javascript] 262 | ---- 263 | function initialize() { 264 | export default 'hello!' // SyntaxError 265 | } 266 | initialize() 267 | ---- 268 | 269 | There are a few other ways of exposing an API in ESM, besides `export default` statements. 270 | 271 | ===== Named exports 272 | 273 | When ((("export statements", "named exports")))((("named exports")))you want to expose multiple values from ((("CommonJS", "module.exports")))((("module.exports")))CJS modules you don't necessarily need to explicitly export an object containing every one of those values. You could simply add properties onto the implicit `module.exports` object. There's still a single binding being exported, containing all properties the `module.exports` object ends up holding. While the following example exports two individual values, both are exposed as properties on the exported object. 274 | 275 | 276 | [source,javascript] 277 | ---- 278 | module.exports.counter = 0 279 | module.exports.count = () => module.exports.counter++ 280 | ---- 281 | 282 | 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. 283 | 284 | [source,javascript] 285 | ---- 286 | export let counter = 0 287 | export const count = () => counter++ 288 | ---- 289 | 290 | Note that the last bit of code cannot be refactored to extract the variable declarations into standalone statements that are later passed to `export` as a named export, as that'd be a syntax error. 291 | 292 | [source,javascript] 293 | ---- 294 | let counter = 0 295 | const count = () => counter++ 296 | export counter // SyntaxError 297 | export count 298 | ---- 299 | 300 | By being rigid in how its declarative module syntax works, ESM favors static analysis, once again at the expense of flexibility. Flexibility inevitably comes at the cost of added complexity, which is a good reason not to offer flexible interfaces. 301 | 302 | ===== Exporting lists 303 | 304 | ES6 modules ((("export statements", "lists")))((("lists, exporting")))let you `export` lists of named top-level members, as seen in the following snippet. The syntax for export lists is easy to parse, and presents a solution to the problem we observed in the last code snippet from the previous section. 305 | 306 | [source,javascript] 307 | ---- 308 | let counter = 0 309 | const count = () => counter++ 310 | export { counter, count } 311 | ---- 312 | 313 | If you'd like to export a binding but give it a different name, you can use the aliasing syntax: `export { count as increment }`. In doing so, we're exposing the `count` binding from the local scope as a public method under the `increment` alias, as the following snippet shows. 314 | 315 | [source,javascript] 316 | ---- 317 | let counter = 0 318 | const count = () => counter++ 319 | export { counter, count as increment } 320 | ---- 321 | 322 | Finally, we can specify a default export when using the named member list syntax. The next bit of code uses `as default` to define a default export at the same time as we're enumerating named exports. 323 | 324 | [source,javascript] 325 | ---- 326 | let counter = 0 327 | const count = () => counter++ 328 | export { counter as default, count as increment } 329 | ---- 330 | 331 | The following piece of code is equivalent to the previous one, albeit a tad more verbose. 332 | 333 | [source,javascript] 334 | ---- 335 | let counter = 0 336 | const count = () => counter++ 337 | export default counter 338 | export { count as increment } 339 | ---- 340 | 341 | It's important to keep in mind that we are exporting bindings, and not merely values. 342 | 343 | ===== Bindings, not values 344 | 345 | ES6 modules ((("export statements", "bindings")))((("bindings, exporting")))export bindings, not values or 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. 346 | 347 | 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. 348 | 349 | [source,javascript] 350 | ---- 351 | export let fungible = { name: 'bound' } 352 | setTimeout(() => fungible = [0, 1, 2], 5000) 353 | ---- 354 | 355 | Modules consuming this API would see the `fungible` value changing after five seconds. Consider the following example, where we print the consumed binding every two seconds. 356 | 357 | [source,javascript] 358 | ---- 359 | import { fungible } from './fungible.js' 360 | 361 | console.log(fungible) // <- { name: 'bound' } 362 | setInterval(() => console.log(fungible), 2000) 363 | // <- { name: 'bound' } 364 | // <- { name: 'bound' } 365 | // <- [0, 1, 2] 366 | // <- [0, 1, 2] 367 | // <- [0, 1, 2] 368 | ---- 369 | 370 | 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 unexpectedly from the point of view of a consumer. 371 | 372 | The JavaScript module system also offers an `export..from` syntax, where you can expose another module's interface. 373 | 374 | ===== Exporting from another module 375 | 376 | We can ((("export statements", "from")))expose another module's named exports using by adding a `from` clause to an `export` statement. The bindings are not imported into the local scope: our module acts as a pass-through where we expose another module's bindings without getting direct access to them. 377 | 378 | 379 | [source,javascript] 380 | ---- 381 | export { increment } from './counter.js' 382 | increment() 383 | // ReferenceError: increment is not defined 384 | ---- 385 | 386 | You can give aliases to named exports, as they pass through your module. If the module in the following example were named `aliased`, then consumers could `import { add } from './aliased.js'` to get a reference to the `increment` binding from the `counter` module. 387 | 388 | [source,javascript] 389 | ---- 390 | export { increment as add } from './counter.js' 391 | ---- 392 | 393 | An ESM module could also expose every single named export found in another module by using a wildcard, as shown in the next snippet. Note that this wouldn't include the default binding exported by the `counter` module. 394 | 395 | [source,javascript] 396 | ---- 397 | export * from './counter.js' 398 | ---- 399 | 400 | When we want to expose another module's `default` binding, we'll have to use the named export syntax adding an alias. 401 | 402 | [source,javascript] 403 | ---- 404 | export { default as counter } from './counter.js' 405 | ---- 406 | 407 | We've now covered every way in which we can expose an API in ES6 modules. Let's jump over to `import` statements, which can be used to ((("ES6 modules", "export statements", startref="esm8es")))((("export statements", startref="exs8")))consume other modules. 408 | 409 | 410 | ==== import Statements 411 | 412 | We ((("ES6 modules", "import statements", id="esm8is")))((("import statements", id="is8")))can load a module from another one using `import` statements. The way modules are loaded is implementation-specific; that is, it's not defined by the specification. We can write spec-compliant ES6 code today while smart people figure out how to deal with module loading in browsers. 413 | 414 | Compilers like ((("Babel")))Babel are able to concatenate modules with the aid of a module system like CommonJS. That means `import` statements in Babel mostly follow the same semantics as `require` statements in CommonJS. 415 | 416 | Let's suppose we have the following code snippet in a _./counter.js_ module. 417 | 418 | [source,javascript] 419 | ---- 420 | let counter = 0 421 | const increment = () => counter++ 422 | const decrement = () => counter-- 423 | export { counter as default, increment, decrement } 424 | ---- 425 | 426 | The statement in the following code snippet could be used to load the `counter` module into our `app` module. It won't create any variables in the `app` scope. It will execute any code in the top level of the `counter` module, though, including that module's own `import` statements. 427 | 428 | [source,javascript] 429 | ---- 430 | import './counter.js' 431 | ---- 432 | 433 | In the same fashion as `export` statements, `import` statements are only allowed in the top level of your module definitions. This limitation helps compilers simplify their module loading capabilities, as well as help other static analysis tools parse your codebase. 434 | 435 | ===== Importing default exports 436 | 437 | CommonJS ((("import statements", "default exports")))((("default exports, importing")))modules let you import other modules using `require` statements. When we need a reference to the default export, all we'd have to do is assign that to a variable. 438 | 439 | 440 | [source,javascript] 441 | ---- 442 | const counter = require('./counter.js') 443 | ---- 444 | 445 | To import the default binding exported from an ES6 module, we'll have to give it a name. The syntax and semantics are a bit different than what we use when declaring a variable, because we're importing a binding and not just assigning values to variables. This distinction also makes it easier for static analysis tools and compilers to parse our code. 446 | 447 | [source,javascript] 448 | ---- 449 | import counter from './counter.js' 450 | console.log(counter) 451 | // <- 0 452 | ---- 453 | 454 | Besides default exports, you could also import named exports and alias them. 455 | 456 | ===== Importing named exports 457 | 458 | The ((("import statements", "named exports", id="is8ne")))following bit of code shows how we can import the `increment` method from our `counter` module. Reminiscent of assignment destructuring, the syntax for importing named exports is wrapped in braces. 459 | 460 | [source,javascript] 461 | ---- 462 | import { increment } from './counter.js' 463 | ---- 464 | 465 | To import multiple bindings, we separate them using commas. 466 | 467 | [source,javascript] 468 | ---- 469 | import { increment, decrement } from './counter.js' 470 | ---- 471 | 472 | The syntax and semantics are subtly different from destructuring. While destructuring relies on colons to create aliases, `import` statements use an `as` keyword, mirroring the syntax in `export` statements. The following statement imports the `increment` method as `add`. 473 | 474 | [source,javascript] 475 | ---- 476 | import { increment as add } from './counter.js' 477 | ---- 478 | 479 | You can combine a default export with named exports by separating them with a comma. 480 | 481 | [source,javascript] 482 | ---- 483 | import counter, { increment } from './counter.js' 484 | ---- 485 | 486 | You can also explicitly name the `default` binding, which needs an alias. 487 | 488 | [source,javascript] 489 | ---- 490 | import { default as counter, increment } from './counter.js' 491 | ---- 492 | 493 | The following example demonstrates how ESM semantics differ from those of CJS. Remember: we're exporting and importing bindings, and not direct references. For practical purposes, you can think of the `counter` binding found in the next example as a property getter that reaches into the `counter` module and returns its local `counter` variable. 494 | 495 | [source,javascript] 496 | ---- 497 | import counter, { increment } from './counter.js' 498 | console.log(counter) // <- 0 499 | increment() 500 | console.log(counter) // <- 1 501 | increment() 502 | console.log(counter) // <- 2 503 | ---- 504 | 505 | Lastly, there are also namespace ((("import statements", "named exports", startref="is8ne")))imports. 506 | 507 | 508 | ===== Wildcard import statements 509 | 510 | We can ((("import statements", "wildcards")))((("wildcard import statements")))import the namespace object for a module by using a wildcard. Instead of importing the named exports or the default value, it imports everything at once. Note that the pass:[*] must be followed by an alias where all the bindings will be placed. If there was a `default` export, it'll be placed in the namespace binding ((("ES6 modules", "import statements", startref="esm8is")))((("import statements", startref="is8")))as well. 511 | 512 | 513 | [source,javascript] 514 | ---- 515 | import * as counter from './counter.js' 516 | counter.increment() 517 | counter.increment() 518 | console.log(counter.default) // <- 2 519 | ---- 520 | 521 | ==== Dynamic import() 522 | 523 | At the ((("dynamic import()", id="di8")))((("ES6 modules", "dynamic import()", id="esm8di")))time of this writing, a proposal for dynamic ++import()++pass:[Check out the proposal specification draft.] expressions is sitting at stage 3 of the TC39 proposal review process. Unlike `import` statements, which are statically analyzed and linked, `import()` loads modules at runtime, returning a promise for the module namespace object after fetching, parsing, and executing the requested module and all of its dependencies. 524 | 525 | The module specifier can be any string, like with `import` statements. Keep in mind `import` statements only allow statically defined plain string literals as module specifiers. In contrast, we're able to use template literals or any valid JavaScript expression to produce the module specifier string for `import()` function calls. 526 | 527 | Imagine you're looking to internationalize an application based on the language provided by user agents. You might statically import a `localizationService`, and then dynamically import the localized data for a given language using `import()` and a module specifier built using a template literal that interpolates `navigator.language`, as shown in the following example. 528 | 529 | [source,javascript] 530 | ---- 531 | import localizationService from './localizationService.js' 532 | import(`./localizations/${ navigator.language }.json`) 533 | .then(module => localizationService.use(module)) 534 | ---- 535 | 536 | Note that writing code like this is generally a bad idea for a number of reasons: 537 | 538 | - It can be challenging to statically analyze, given that static analysis is executed at build time, when it can be hard or impossible to infer the value of interpolations such as `${ navigator.language }`. 539 | - It can't be packaged up as easily by JavaScript bundlers, meaning the module would probably be loaded asynchronously while the bulk of our application has been loaded. 540 | - It can't be tree-shaken by tools like Rollup, which can be used to remove module code that's never imported anywhere in the codebase--and thus never used--reducing bundle size and improving performance. 541 | - It can't be linted by `eslint-plugin-import` or similar tools that help identify module import statements where the imported module file doesn't exist. 542 | 543 | Just like with `import` statements, the mechanism for retrieving the module is unspecified and left up to the host environment. 544 | 545 | The proposal does specify that once the module is resolved, the promise should fulfill with its namespace object. It also specifies that whenever an error results in the module failing to load, the promise should be rejected. 546 | 547 | This allows for loading noncritical modules asynchronously, without blocking page load, and being able to gracefully handle failure scenarios when such a module fails to load, as demonstrated next. 548 | 549 | [source,javascript] 550 | ---- 551 | import('./vendor/jquery.js') 552 | .then($ => { 553 | // use jquery 554 | }) 555 | .catch(() => { 556 | // failed to load jquery 557 | }) 558 | ---- 559 | 560 | We could load multiple modules asynchronously using `Promise.all`. The following example imports three modules and then leverages destructuring to reference them directly in the `.then` clause. 561 | 562 | [source,javascript] 563 | ---- 564 | const specifiers = [ 565 | './vendor/jquery.js', 566 | './vendor/backbone.js', 567 | './lib/util.js' 568 | ] 569 | Promise 570 | .all(specifiers.map(specifier => import(specifier))) 571 | .then(([$, backbone, util]) => { 572 | // use modules 573 | }) 574 | ---- 575 | 576 | In a similar fashion, you could load modules using synchronous loops or even `async`/`await`, as demonstrated next. 577 | 578 | [source,javascript] 579 | ---- 580 | async function load() { 581 | const { map } = await import('./vendor/jquery.js') 582 | const $ = await import('./vendor/jquery.js') 583 | const response = await fetch('/cats') 584 | const cats = await response.json() 585 | $('
    ') 586 | .addClass('container cats') 587 | .html(map(cats, cat => cat.htmlSnippet)) 588 | .appendTo(document.body) 589 | } 590 | load() 591 | ---- 592 | 593 | Using `await import()` makes dynamic module loading look and feel like static `import` statements. We need to watch out and remind ourselves that the modules are asynchronously loaded one by one, though. 594 | 595 | Keep in mind that `import` is function-like, but it has different semantics from regular functions: `import` is not a function definition, it can't be extended, it can't be assigned properties, and it can't be destructured. In this sense, `import()` falls in a similar category as the `super()` call that's available in class ((("dynamic import()", startref="di8")))((("ES6 modules", "dynamic import()", startref="esm8di")))constructors. 596 | 597 | === Practical Considerations for ES Modules 598 | 599 | When ((("ES6 modules", "considerations for", id="esm8cf")))using a module system, any module system, we gain the ability of explicitly publishing an API while keeping everything that doesn't need to be public in the local scope. Perfect information hiding like this is a sought-out feature that was previously hard to reproduce: you'd have to rely on deep knowledge of JavaScript scoping rules, or blindly follow a pattern inside which you could hide information, as shown next. In this case, we create a `random` module with a locally scoped `calc` function, which computes a random number in the `[0, n)` range; and a public API with the `range` method, which computes a random number in the `[min, max]` range. 600 | 601 | [source,javascript] 602 | ---- 603 | const random = (function() { 604 | const calc = n => Math.floor(Math.random() * n) 605 | const range = (max = 1, min = 0) => calc(max + 1 - min) + min 606 | return { range } 607 | })() 608 | ---- 609 | 610 | Compare that to the following piece of code, used in an ESM module called `random`. The Immediately Invoked Function Expression (IIFE) ((("IIFE (Immediately Invoked 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 `