15

As I understand it (see section 16.3.2.1), ES6 allows different syntaxes for function / class export operands. The difference refers to whether the exported function needs to be interpreted at import as a function declaration, in which case you write: export default function () {} // (a) or as a function expression: export default (function () {}); // (b).

As a possible related sidenote: I read that imports are hoisted, but I'm not really sure what that means in this context.

Taking the case of this example:

import foo from 'my_module'; // (c)

As I understand it, the above statement will save my exported function in a foo variable. Is that variable hoisted, or what is, and when?

Most importantly, what is the difference (in terms of setting foo) when my_module exports the function using (a) and when it exports it using (b)?

c10b10
  • 357
  • 5
  • 16

1 Answers1

24

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.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Your explanation of hoisting is exactly what I was looking for, thank you. It seems that I haven't stated my central question well enough though. I edited the main post to make it clearer. – c10b10 Feb 05 '16 at 15:34
  • @c10b10: Ah ok. I'll edit later. For now: the difference is absolute minimalistic, but you definitely should use the declaration. – Bergi Feb 05 '16 at 15:44
  • Whenever you have time, I'd love to read why. As a related sidenote, in a non-module context, aren't function expressions less taxing on the memory? – c10b10 Feb 05 '16 at 15:48
  • @c10b10: A function object is a function object. How it was created (expression, declaration, `new Function()`) doesn't matter wrt to memory. – Felix Kling Feb 05 '16 at 18:19
  • In that case, why would it matter how you export it? Why have both syntaxes? – c10b10 Feb 05 '16 at 19:36
  • @Bergi the second `import` case with brackets - works for me, there are no errors. I am using babel & webpack – The Reason Feb 09 '16 at 13:31
  • Probably you meant `import * as def from "myself";` then in this case it doesnt work – The Reason Feb 09 '16 at 14:07
  • @The: babel doesn't seem to recognise the anonymous function declaration at all and treats it like the function expression. Which means that both shouldn't work. And of course this might be affected by the module loader/bundler and how its implementation treats recursive dependencies. Can you paste the transpilation result somewhere? And no, I did mean `import def from …; def();`, although `import * as x from …; x.default()` would be equivalent. – Bergi Feb 09 '16 at 14:14
  • @Bergi Follow to ***[this](https://jsfiddle.net/ru1bgaL8/)*** link, and that's why i asked you about `* as def`.. thanks – The Reason Feb 09 '16 at 14:19
  • @The: You seem to have misunderstood my answer. I meant a module that *imports itself* (its own export) - which doesn't make sense of course - and looks exactly like the snippet I posted. That babel output you pasted in the fiddle is from two modules, where one imports the other, and as I said there is no observable difference in that case. – Bergi Feb 09 '16 at 14:51
  • @Bergi oh, now i understand, sorry for confusion – The Reason Feb 09 '16 at 15:50