You-dont-know-js: "es6 & beyond": ch3, default exports

Created on 9 May 2016  路  7Comments  路  Source: getify/You-Dont-Know-JS

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 () { ... });.

question

Most helpful comment

declaration

Given:

// exporter.js
export default function foo() {};

We get a HoistableDeclaration form of ExportDeclaration

Which has the relevant ExportEntrys:

| [[LocalName]] | [[ExportName]] |
| --- | --- |
| foo | default |

Given:

// importer.js
import bar,* as ns from "./exporter.js";

Which has the relevant ImportEntrys:

| [[LocalName]] | [[ImportName]] |
| --- | --- |
| bar | default |
| ns | * |

hooking them up

There are 2 ways to access the exported variable:

Using the import binding

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".

Using the ModuleNamespaceObject

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

Conclusion

Changing foo in exporter.js is visible from importer.js, bar and ns.default will both use the new value when accessed.

All 7 comments

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.

declaration

Given:

// exporter.js
export default function foo() {};

We get a HoistableDeclaration form of ExportDeclaration

Which has the relevant ExportEntrys:

| [[LocalName]] | [[ExportName]] |
| --- | --- |
| foo | default |

Given:

// importer.js
import bar,* as ns from "./exporter.js";

Which has the relevant ImportEntrys:

| [[LocalName]] | [[ImportName]] |
| --- | --- |
| bar | default |
| ns | * |

hooking them up

There are 2 ways to access the exported variable:

Using the import binding

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".

Using the ModuleNamespaceObject

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

Conclusion

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

garrettwgg picture garrettwgg  路  5Comments

aszx87410 picture aszx87410  路  3Comments

vineetpanwar picture vineetpanwar  路  3Comments

laoshaw picture laoshaw  路  3Comments

williamvittso picture williamvittso  路  3Comments