Ambiguity with default exports and live bindings?

# /#!/JoePea (8 years ago)

Is it true one of the following does not create a live binding?

let A = 123
export default A // not a live binding?

compared to

let A = 123
export {A as default} // live binding?

If so, this seems like large source for unexpected behavior when people create modules. I can imagine people easily overlooking the difference and expecting live bindings in both cases (this already happened to me).

# Logan Smyth (8 years ago)

From the code perspective, changing the value of A will not update the

value of the export, but it's not quite that it's not live, it's that you're not exporting what you think you're exporting.

export default A;

is essentially short for

const fakeInaccessibleVariable = A;
export {fakeInaccessibleVariable as default};

Changing the value of the A binding at a later time has no effect on the exported value because it won't change the value if the inaccessible variable.

In the spec this variable is referred to as *default* (note the *s in the name mean it's not possible to have a program with that as a variable name, making collisions with a variable impossible). This is because export default A; falls into the same category as export default 123;, where you are exporting an arbitrary value as the default. In the case of export default A; you are exporting the value of A at the time that the export default expression is evaluated. In the spec this is the format export default [lookahead ∉ { function, class }] AssignmentExpression[In]; You can see this syntax construct's behavior defined in the last block in www.ecma-international.org/ecma-262/7.0/#sec-exports-runtime-semantics-evaluation .

# Bergi (8 years ago)

/#!/JoePea schrieb:

Is it true one of the following does not create a live binding?

Yes and no.

let A = 123
export default A // not a live binding?

Actually it does create a live binding - to the variable with the name "default" which you cannot assign. But yes, the binding A is not exported.

let A = 123
export {A as default} // live binding?

Yes.

If so, this seems like large source for unexpected behavior when people create modules. I can imagine people easily overlooking the difference and expecting live bindings in both cases (this already happened to me).

I think people do in general not expect live bindings at all. For declarations and the purpose of initialisation order, yes, but not for mutable variables. You should export consts. See also stackoverflow.com/q/35223111/1048572?what-is-the-difference-between-importing-a-function-expression-or-a-function-declaration-from-a-ES6-module.

Kind , Bergi

# /#!/JoePea (8 years ago)

Interesting. I think it is very intuitive to think that export default A where A is a variable would be live. I personally find that live bindings are very helpful when dealing with circular dependencies.

For example, suppose we have two modules A and B:

// B.js
import A, {defineClassA} from `./A`

if (!A) defineClassA() // this fires, and triggers the error, see A.js.

export default
class B extends A { /* ... */ }
// A.js
import someFunction from './other-module'
import B from './B'

let A = null
export default A

if (!A) defineClassA() // this doesn't fire, the error is triggered by B.js
first.

export
function defineClassA() {
  someFunction() // Error happens here, triggered from B.js

  A = class {
    method() {
      console.log('B constructor:', B)
    }
  }
}

I was erroneously expecting the export statements to create live bindings, so that my defineClassA call inside of B would cause A to be defined in case module B evaluates first. But as you can see, that doesn't work.

So, I'll simply now prefer export {A as default}, in which case the modules become

// B.js
import A, {defineClassA} from `./A`

if (!A) defineClassA() // this fires, and triggers the error, see A.js.

class B extends A { /* ... */ }

export {B as default}
// A.js
import someFunction from './other-module'
import B from './B'

let A = null
export {A as default}

if (!A) defineClassA() // this doesn't fire, the error is triggered by B.js
first.

export
function defineClassA() {
  someFunction() // Error happens here, triggered from B.js

  A = class {
    method() {
      console.log('B constructor:', B)
    }
  }
}

Although I love ES6 modules, I don't think these live binding semantics are clear or intuitive. Although probably too late and it won't happen, what if export bindings were more explicit with a live keyword?

export {
  live bar, // live binding of variable bar
  foo // not live, exports value.
}

export // not live
function foo() {}

export live // live
function foo() {}

export default live A // A is a live binding

export default A // exports value

// etc...

The reason I suggest this is because JS seems to prefer having meaningful keywords to make intent clear (for example, we chose to have the await keyword inside async functions, but we could have gone the Java route and used no keywords at all to make it hard to immediately see what is async and what isn't).

# /#!/JoePea (8 years ago)

I think I'm more confused than I thought. Are all bindings read only always, only modifiable by exported functions/classes?

# Allen Wirfs-Brock (8 years ago)

On Jul 10, 2016, at 12:46 PM, /#!/JoePea <joe at trusktr.io> wrote:

I think I'm more confused than I thought. Are all bindings read only always, only modifiable by exported functions/classes?

All imported bindings are read-only. See tc39.github.io/ecma262/#sec-module-environment-records, tc39.github.io/ecma262/#sec-module-environment-records