What is the difference between importing a function expression or a function declaration from a ES6 module?

Solution 1:

Your question is a bit convoluted but I'll try my best to explain everything.

Let's first establish how modules work in general. A module has a set of exported names, each of which refer to a local variable in that module. The name of the export does not need to be the same as that of the local binding. One of the exported names can be default, for which there is special syntax (both in exporting and importing) dedicated for the case that a module only exports a single thing.

I read that imports are hoisted, but I'm not really sure what that means in this context:

import { foo } from 'my_module';

Yes, import declarations are hoisted. Similarly to a var or function (and actually like every other declaration) the identifier foo is available right from the beginning, before any statements in the module are executed. In fact the binding is even created before those of declared variables.

The difference is how they are initialised:

  • vars are initialised with undefined
  • functions and function*s are initialised with the function object
  • let, const and classes are left uninitialised
  • imported bindings are not even really initialised, they are created as a pointer to the local variable that the exported name refers to in the imported module
  • imported modules (import * as …) are initialised with a module object (whose properties are such pointers as well)

When is foo set to refer to my exported function?

The short answer: before everything else.

The long answer: it's not really set. It's a reference to the local variable in the imported module that you expect to hold the function. The local variable might change when it's not const - but we usually don't expect that of course. And normally it does contain that function already, because the imported module is completely evaluated before the module(s) that import it are. So if you fear there's a problem with var functionName = function() {} vs function functionName() {} you may be relieved - there is not.

Now back to your title question:

What is the difference between exporting a function expression and a function declaration in a ES6 module?

Nothing special, the two aspects actually don't have much to do with each other:

  • export declarations link an export name to a local variable in the module scope
  • All variables in the module scope are hoisted, as usual
  • function declarations are initialised differently than variable declarations with an assignment of a function expression, as usual

Of course, there still are no good reasons not to use the more declarative function declarations everywhere; this is not different in ES6 modules than before. If at all, there might even be less reasons to use function expressions, as everything is covered by declarations:

/* for named exports */
export function foo() {…}

// or
function foo() {…}
export {foo as foo}

/* for default exports */
export default function foo() {…}

// or
function foo() {…}
export {foo as default}

// or
function foo() {…}
export default foo;

// or
export default function() {…}

Ok, the last two default export declarations are actually a bit different than the first two. The local identifier that is linked to the exported name default is not foo, but *default* - it cannot be reassigned. This makes sense in the last case (where there is no name foo), but in the second-to-last case you should notice that foo is really just a local alias, not the exported variable itself. I would recommend against using this pattern.

Oh, and before you ask: Yes, that last default export really is a function declaration as well, not an expression. An anonymous function declaration. That's new with ES6 :-)

So what exactly is the difference between export default function () {} and export default (function () {});

They are pretty much the same for every purpose. They're anonymous functions, with a .name property "default", that are held by that special *default* binding to to which the exported name default points to for anonymous export values.
Their only difference is hoisting - the declaration will get its function instantiated at the top of the module, the expression will only be evaluated once the execution of module code reaches the statement. However, given that there is no variable with an accessible name for them, this behavior is not observable except for one very odd special case: a module that imports itself. Um, yeah.

import def from "myself";
def(); // works and logs the message
export default function() {
    console.log("I did it!");
}

import def from "myself";
def(); // throws a TypeError about `def` not being a function
export default (function() {
    console.log("I tried!");
});

You really shouldn't do either of these things anyway. If you want to use an exported function in your module, give it a name in its declaration.

In that case, why have both syntaxes?

Happens. It's allowed by the spec because it doesn't make extra exceptions to prohibit certain nonsensical things. It is not intended to be used. In this case the spec even explicitly disallows function and class expressions in export default statements and treats them as declarations instead. By using the grouping operator, you found a loophole. Well done. Don't abuse it.