` 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 `