Starting with ES6, we can use import and export to use the module system to better organize code. However, in the process of use, we inevitably encounter some problems, this article on some of the problems are studied.

Note: This article focuses on the original ESM module system. Some packaging systems (Webpack, etc.) convert ESM to CJS, resulting in the loss of ESM features. Test environment: Chrome 99.

1. Export variables or values

The default is derived

When we declare a variable in a module, but the initial value of the variable needs to be obtained in an asynchronous operation, such as:

// a.js
let x = 0

setTimeout(() = > {
  x = 1
})

export default x
Copy the code

Where x is used:

import x from './a.js'

console.log(x) / / 0
setTimeout(() = > {
  console.log(x) / / 0
})
Copy the code

We’ll see that the value of x printed out is 0.

For the default export, the value of x is exported, not the variable x itself. The equivalent is that the module assigns x to a special internal variable, which it exports all the time thereafter.

Named after the export

If we use named export:

// a.js
let x = 0

setTimeout(() = > {
  x = 1
})

export { x }
Copy the code

Where a is used:

import { x } from './a.js'

console.log(x) / / 0
setTimeout(() = > {
  console.log(x) / / 1
})
Copy the code

We’ll see that we print a 0, then a 1. Because when console.log is executed in setTimout, x is already assigned to 1, and the name exports the variable itself, similar to a pointer in C.

Reference types

This is not a problem for reference types (such as a {}), because the value of the reference type is exported by default, but the value is not the actual content of the object, but the address of the object to which the variable points, similarly to Pointers.

Name default export

If we want to use default imports, but want to export variables, we can write it like this:

// a.js
export { x as default }
Copy the code
import x from './a.js'
Copy the code

2. Space import

We sometimes use space to import all variables if we have a large number of named exported variables:

let x = 0

setTimeout(() = > {
  x = 1
})

export default x
export { x }
Copy the code
import * as a from './a.js'
Copy the code

Then use the exported variable as if it were an object member:

console.log(a.x) / / 0
setTimeout(() = > {
  console.log(a.x) / / 1
})
Copy the code

A.x prints 0 and then 1. The exported Module is not thought of as a simple object; it is actually a “Module” type “variable”; even if the member X is given a literal, it will still change depending on the variable it points to.

Also, the members in this module variable are read-only.

If we print a in Chrome, we can get an object like this:

Module {Symbol(Symbol.toStringTag): 'Module'}
    default: 0
    x: 1
    Symbol(Symbol.toStringTag): "Module"
Copy the code

Here, default is the member corresponding to the default export, and whether it represents a value or a pointer depends on how default is exported.

3. Circular dependencies

If a module A introduces a module B, and B introduces a, there is a circular dependency between them. The ESM module solves this problem through static checking.

If there are two modules:

// a.js
import y from './b.js'

let x = 0

export default x
export { y }
Copy the code
// b.js
import x from './a.js'

let y = 0

export default y
export { x }
Copy the code

The two modules introduce default exports to each other, but the ESM module system is static. The interpreter first checks all the code in A. js and finds the declaration of X. Then check the b.js again and find the declaration of ‘y’. Both meet the requirements, so they are introduced into their scope and the variable is derived.

When a third module is introduced into one of the modules, for example:

import x, { y } from './a.js'

console.log(x, y) / / 0, 0
Copy the code

Temporary dead zone

But if you use the introduced variables directly in A.js and b.js:

// a.js
import y from './b.js'

let x = y

export default x
export { y }
Copy the code
// b.js
import x from './a.js'

let y = x

export default y
export { x }
Copy the code

Fetching x from the browser will result in an error:

Uncaught ReferenceError: Cannot access 'x' before initialization
Copy the code

The reason is that x and Y are declared using let and have a temporary dead zone that cannot be used before the declaration, not a module reference error.

Variable ascension

If we declare x and y using var instead and use named exports, the module can be imported successfully. The reason for this is the variable promotion of var, which allows variables to be used before declaration and names export Pointers to variables. Except that the variable is not assigned, so the value is undefined.

// a.js
import { y } from './b.js'

var x = y

export default x
export { y }
Copy the code
// b.js
import { x } from './a.js'

var y = x

export default y
export { x }
Copy the code

But even the default export declared by var is still unusable, for the same reason as above, because the default exported variable is stored in a special variable of the module, and there is no variable promotion.

That is, as long as the variable with the variable promotion is not directly used, there will be no error. For example, the default exported function:

// a.js
import y from './b.js'

function x() {
  console.log(y)
}

export default x
export { y }
Copy the code
// b.js
import x from './a.js'

function y() {
  console.log(x)
}

export default y
export { x }
Copy the code

There is no problem with the module. But if y is called directly in a.js:

// a.js
import y from './b.js'

function x() {
  console.log(y)
}
y()

export default x
export { y }
Copy the code

The same thing happens.

4. Influence of top-level await on modules

In systems that support the new “top-level-await” syntax, the top-level await makes the entire module an asynchronous operation, thus exporting the latest value of the variable by default. Such as:

let x = 0

await new Promise(resolve= >
  setTimeout(() = > {
    x = 1

    resolve()
  })
)

setTimeout(() = > {
  x = 2

  resolve()
})

export default x
Copy the code
import x from './a.js'

console.log(x)
setTimeout(() = > {
  console.log(x)
})
Copy the code

X prints always 1. Even if you change the value of x to 2 in the second setTimeout of A. js, it will not affect the external x already imported by default, because they are not essentially the same variable.