I finally figured out what it was that's been bugging me about ES6 modules.
It's like this. These two snippets of code do exactly the same thing:
const myModule = require('./my-module') const { a, b } = myModule
const { a, b } = require('./my-module')
However, these two snippets of code do not (necessarily) do the same thing:
import myModule from './my-module' const { a, b } = myModule
import { a, b } from './my-module'
In the first scenario (snippets P and Q), we're working with CommonJS modules. A CommonJS module always exports exactly one value. If my-module.js
is a CommonJS module like:
module.exports = { a: 1, b: 2 }
then...
const myModule = require('./my-module') // `myModule` is { a: 1, b: 2 } const { a, b } = myModule // object destructuring // `a` is 1 // `b` is 2
const { a, b } = require('./my-module') // object destructuring // `a` is 1 // `b` is 2
...the eventual values of a
and b
are the same.
Equally, if my-module.js
says something like module.exports = null
, snippets P and Q would fail on attempted destructuring. But they would fail in the same way, for the same reason.
But in the second scenario (snippets X and Y), we're working with ES6 modules. Unike a CommonJS module, an ES6 module exports multiple different values:
default
).So, if my-module.js
is an ES6 module like:
// default export export default { a: 1, b: 2 } // and some named exports export const a = 3 export const b = 4
then...
import myModule from './my-module' // `myModule` is { a: 1, b: 2 } const { a, b } = myModule // object destructuring // `a` is 1 // `b` is 2
import { a, b } from './my-module' // `a` is 3 // `b` is 4
...the eventual values of a
and b
are not the same.
The moral of this story?
This is not object destructuring:
import { a, b } from './my-module'
It looks like it is, but it isn't! It certainly is not destructuring of the default export of my-module.js
. (That default export may not even exist. It could be undefined
.) It is actually importing named exports from my-module.js
.
If you actually want to get an object containing all the named exports, what we have to do instead is:
import * as myModule from './my-module' // `myModule` is { default: { a: 1, b: 2 }, a: 3, b: 4 } const { a, b } = myModule // object destructuring // `a` is 3 // `b` is 4
.default
Now, suppose we we want an automated process to turn an arbitrary ES6 module into a CommonJS module. Say, because we're writing a transpiler like Babel which we can use to make ES6 and CommonJS modules interoperable. Example input:
export default { a: 1, b: 2 } export const a = 3 export const b = 4
There's a variety of "obvious" things we could do here which would be wrong. We can't just module.export
the default export alone, because then the consumer can't access 3
or 4
. We can't add the named exports as properties to the default export object, because the two a
s and b
s would collide with one another.
What we find is that the most, arguably only, logical way to do this is to have the output CommonJS module export a single object containing all the original ES6 module's named exports along with its default export:
module.exports = { default: { a: 1, b: 2 }, a: 3, b: 4 }
This is, in fact, how most if not all ES6-to-CommonJS transpilers work.
And now we have an unfortunate scenario. Previously, with CommonJS modules, it was possible to set module.exports
to be literally any JavaScript value we wished, such as a function:
module.exports = () => 5
and CommonJS consumers could do something cute like:
const getValue = require('./my-module') // `getValue` is () => 5 const value = getValue() // `value` is 5
But now that we have upgraded my-module.js
from a CommonJS module to an ES6 module, it is impossible for us to structure our new ES6 module in such a way that when consumers transpile the ES6 back to CommonJS they still see the old behaviour and don't have to change anything. module.exports
simply cannot be a function anymore. It has to be an object.
This is the best we can do:
export default () => 5
Compiled to CommonJS, that becomes:
module.exports = { default: () => 5 }
The old CommonJS consumer's code was:
const getValue = require('./my-module') // `getValue` is { default: () => 5 } const value = getValue() // throws an exception, the object is not callable
and it has to be refactored to:
const getValue = require('./my-module').default // `getValue` is () => 5 const value = getValue() // `value` is 5
So, that's why.
Discussion (3)
2020-01-16 01:05:46 by qntm:
2020-07-17 11:50:17 by hoichi:
2020-07-17 11:51:13 by hoichi: