There's a subtle nuance to default export syntax that you should pay close attention to. Compare these two snippets:
function foo(..) {
// ..
}export default foo;
And this one:function foo(..) {
// ..
}export { foo as default };
In the first snippet, you are exporting a binding to the function expression value at that moment, not to the identifier foo. In other words, export default .. takes an expression. If you later assign foo to a different value inside your module, the module import still reveals the function originally exported, not the new value.By the way, the first snippet could also have been written as:
export default function foo(..) {
// ..
}
Warning: Although the function foo.. part here is technically a function expression, for the purposes of the internal scope of the module, it's treated like a function declaration, in that the foo name is bound in the module's top-level scope (often called "hoisting"). The same is true for export default class Foo... However, while you can do export var foo = .., you currently cannot do export default var foo = .. (or let or const), in a frustrating case of inconsistency. At the time of this writing, there's already discussion of adding that capability in soon, post-ES6, for consistency sake.
Is this description correct? This book says a function is a declaration in exports default function () { ... }
, and an expression in export default (function () { ... });
.
It's a strange set of syntactic exceptions. export default function foo() {}
indeed creates a foo
binding in the module's local scope so that it can be called, meaning it acts like a function declaration. But export default ...
in general only works with expressions, as for example export default var a = 2
is invalid.
So, does your description of "If you later assign foo to a different value inside your module, the module import still reveals the function originally exported, not the new value." still apply to the export default function foo(..) { ... }
, or it's only relevant for the case when export default
takes an expression?
That's a good question. I don't think it applies, but I'm not entirely sure.
Given:
// exporter.js
export default function foo() {};
We get a HoistableDeclaration form of ExportDeclaration
Which has the relevant ExportEntry
s:
| [[LocalName]]
| [[ExportName]]
|
| --- | --- |
| foo
| default
|
Given:
// importer.js
import bar,* as ns from "./exporter.js";
Which has the relevant ImportEntry
s:
| [[LocalName]]
| [[ImportName]]
|
| --- | --- |
| bar
| default
|
| ns
| *
|
There are 2 ways to access the exported variable:
console.log(bar)
Is going to be grabbing the value from the environment record. Mostly we just need to know what that binding is. It is made during ModuleDeclarationInstantiation
Let resolution be ? importedModule.ResolveExport(in.[[ImportName]], 芦 禄, 芦 禄).
Is called, which in turn results with
Return Record{[[Module]]: module, [[BindingName]]: e.[[LocalName]]}.
Where e
is an ExportEntry of {[[LocalName]]:foo | [[ExportName]]:default}
.
So! Now we can see [[BindingName]]
is foo
. It will return the value of foo
inside exporter.js
. The question is: does this stay "live".
The answer can be found via the text to CreateImportBinding
Accesses to the value of the new binding will indirectly access the bound value of the target binding.
All accesses will be "live".
Now, what about using ns
?
console.log(ns.default);
Actually uses the same [[BindingName]]
as when an import variable is created. you can see it in ModuleNamespaceObject.[[Get]]
Changing foo
in exporter.js
is visible from importer.js
, bar
and ns.default
will both use the new value when accessed.
Wow, thanks! :)
yup, like @bmeck describes.
If you want default export of a function/class whose binding value can't be changed leave out the function name in the declaration:
export default function () {...} //treated as a hoisted anonymous function dcl (not function expr)
or parenthesize to force a function expression:
export default (function foo() {...foo...});
or use a separate const declaration and a function expr:
const foo = function () {...foo...}; //but note this isn't hoisted
export default foo;
export default function/class was given special treatment out of concern that treating them as function/class expressions (ie, doesn't create a module level binding) is typically not going to be what the programmer intended.
For posterity, let's clear up the question:
So, does your description of "If you later assign foo to a different value inside your module, the module import still reveals the function originally exported, not the new value." still apply to the export default function foo(..) { ... }
No, it does not apply. An export default function foo() { .. }
can later be redefined in the module and the redefinition is what's seen on import side.
or it's only relevant for the case when export default takes an expression?
Yep.
Most helpful comment
declaration
Given:
We get a HoistableDeclaration form of ExportDeclaration
Which has the relevant
ExportEntry
s:|
[[LocalName]]
|[[ExportName]]
|| --- | --- |
|
foo
|default
|Given:
Which has the relevant
ImportEntry
s:|
[[LocalName]]
|[[ImportName]]
|| --- | --- |
|
bar
|default
||
ns
|*
|hooking them up
There are 2 ways to access the exported variable:
Using the import binding
Is going to be grabbing the value from the environment record. Mostly we just need to know what that binding is. It is made during ModuleDeclarationInstantiation
Is called, which in turn results with
Where
e
is an ExportEntry of{[[LocalName]]:foo | [[ExportName]]:default}
.So! Now we can see
[[BindingName]]
isfoo
. It will return the value offoo
insideexporter.js
. The question is: does this stay "live".The answer can be found via the text to CreateImportBinding
All accesses will be "live".
Using the ModuleNamespaceObject
Now, what about using
ns
?Actually uses the same
[[BindingName]]
as when an import variable is created. you can see it inModuleNamespaceObject.[[Get]]
Conclusion
Changing
foo
inexporter.js
is visible fromimporter.js
,bar
andns.default
will both use the new value when accessed.