Ambiguity with default exports and live bindings?
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
.
/#!/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 const
s.
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
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).
I think I'm more confused than I thought. Are all bindings read only always, only modifiable by exported functions/classes?
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
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).