Comparison of ESModule and CommonJS and Precautions

In ES6, we know that import and export replace require and module.exports to import and export modules, but if you don’t understand the characteristics of ES6 modules, the code may run with some unimaginable results. I will use this article. Uncover the features of the ES6 module mechanism for you.

I will not introduce the use of the two in detail. If you are interested, you can take a look at my previous blog:JavaScript Module使用语法

This article mainly addresses the following issues:

  • Do these two output a copy or a reference, and if you copy it, is it a deep copy or a shallow copy?
  • What is the difference between the loading and running times of the two?
  • How do they solve circular dependencies and repeated loading problems?
  • What are the differences between the operating environments of the two, when can they be mixed and why?

Copy

CommonJS

Let’s take a look at the following code and its running result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// b.js
let foo = 1;
let bar = {
a: 1
}
setTimeout(() => {
foo = 2;
bar.a = 2;
}, 500);
module.exports = {
foo: foo,
bar
};

// a.js
const b = require('./b');
console.log(b.foo);
console.log(b.bar.a);
setTimeout(() => {
console.log(b.foo);
console.log(b.bar.a);
}, 1000);

//node a.js result
1
1
1
2

After a introduces b, this code modifies foo and bar inside b, and then prints foo and bar again. You can see that the value of foo has not changed, but bar has changed.

module.exports.foo is not a variable with the internal foo, module.exports.bar is a variable with the internal bar.

But does this mean that CommonJS is a copy or a reference?

Here is a simple explanation, module.exports is exports at the beginning, that is, there is an exports variable, and then we assign module.exports to this exports, so exports.a

The difference is that we require module.exports, not exports, and if we directly modify the module.exports reference, the properties we mount on exports are completely useless.

For example:

exports.a

exports.b

module.exports

If there are only the first two lines, we will get it when we require it.

Let me give you another example, let’s think about the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// b.js
let foo = 1;
let bar = {
a: 1
}
setTimeout(() => {
module.exports.foo = 2;//difference
bar.a = 2;
}, 500);
module.exports = {
foo: foo,
bar
};

// a.js
const b = require('./b');
console.log(b.foo);
console.log(b.bar.a);
setTimeout(() => {
console.log(b.foo);
console.log(b.bar.a);
}, 1000);

//node a.js result
1
1
2//This is 2, why?
2

From the above example, we can see that the result of ** require is actually a reference to module.exports.

  • Why is foo unchanged in the first example and changed in the second one? Because when generating module.exports, foo is a primitive type and is deeply copied to the module.exports.foo, we require module.exports.foo instead of foo inside the inner b.
  • Why is bar always changed, because when generating module.exports, bar is an object, and the address of bar is stored in the module.exports.bar, not a deep copy, so the module.exports.bar we require is the internal bar, and the two of them point to the same memory address

You can also understand it this way:

When we construct module.exports, we are actually a shallow copy, which copies the values to the properties in module.exports.

But when we require, we introduce a reference to module.exports.

ESModule

ESModule was introduced in ES6, so without babel, node cannot be recognized, so we use the browser to execute it.

But if you directly use HTML to import JS, there are two points to note:

Old rules, look at the code

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
< html long = "and" >
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="module" src="./a.js"></script>
</head>
<body>

</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// b.js
export let foo = 1;
setTimeout(() => {
foo = 2;
}, 500)

// a.js
import { foo } from './b.js';
console.log(foo);
setTimeout(() => {
console.log(foo);
import('./b.js').then(({ foo }) => {
console.log(foo);
});
}, 1000);

//result
1
2
2

From this result, the ES6 module is dynamically associating the values in the module, that is, it looks like a reference to the

The reason why I say it seems is because I haven’t seen the specific implementation yet, just from the appearance point of view, it is a quote

What is the difference between the loading and running timing of the two?

ES6 modules are executed at compile time, while CommonJS modules are always loaded at runtime, that is

CommonJS is only introduced when required.

Execution of an ES6 module during compile results in the following two characteristics:

  1. The import command will be statically analyzed by the JavaScript engine and executed before other content within the module.
  2. The export command will have the effect of advancing variable declaration.
1
2
3
4
5
6
7
8
9
10
11
// a.js
console.log('a.js')
import { foo } from './b';

// b.js
export let foo = 1;
Console.log ('b.js execute first');

//Execution result:
//b.js execute first
// a.js

Repeated loading problem

CommonJS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// b.js
let foo = 1;
let bar = {
a: 1
}
setTimeout(() => {
module.exports.foo = 2;
bar.a = 2;
}, 500);
module.exports = {
foo: foo,
bar
};

//a.js
const b = require('./b');
console.log(b.foo);
console.log(b.bar.a);
setTimeout(() => {
console.log(b.foo);
console.log(b.bar.a);

b.foo = 3;
console.log(require('./b').foo);
}, 1000);

// node a.js
1
1
2
2
3

This example shows that each module will only be loaded once, and each time require returns the same reference.

ESModule

This is easy to understand. Regardless of ES6 module, when you repeatedly introduce the same module, the module will only execute once.

1
2
3
4
5
6
7
8
9
// a.js
import './b';
import './b';

// b.js
Console.log ('will only be executed once');

//Execution result:
//will only be executed once

Cyclic dependency problem

Solve the problem of repeated references, then the problem of circular dependencies can be explained

CommonJS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

// node a.js
//Execution result:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done

Combined with the previous characteristics, it is easy to understand. When you want to introduce a module from b, because node has loaded a module before, it will not repeat the execution of a module, but directly generate the current a module. The module.exports object spit out, because a module introduces b module before reassigning done, the value done in the module.exports output from the current a module is still false. When the done value of the b module is output from the a module, the b module has been executed, so the done value in the b module is true.

From the above execution process, we can see that in the CommonJS specification, when encountering a require () statement, the code in the require module will be executed and the result of execution will be cached. It will not be executed repeatedly the next time it is loaded again, but directly take the cached result. Because of this, there will be no infinite loop call when there is a circular dependency. Although this module loading mechanism can avoid the situation that the circular dependency times are wrong, a little carelessness is likely to make the code not execute as we imagined. Therefore, careful planning is still required when writing code to ensure that the dependencies of loop modules work correctly.

ESModule

Like CommonJS modules, ES6 will no longer execute repeatedly loaded modules, and due to the characteristics of ES6 dynamic output binding, it can ensure that ES6 can obtain the current latest values of other modules at any time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// a.js
console.log('a starting')
import {foo} from './b';
console.log('in b, foo:', foo);
export const bar = 2;
console.log('a done');

// b.js
console.log('b starting');
import {bar} from './a';
export const foo = 'foo';
console.log('in a, bar:', bar);
setTimeout(() => {
console.log('in a, setTimeout bar:', bar);
})
console.log('b done');

// babel-node a.js
//Execution result:
// b starting
// in a, bar: undefined
// b done
// a starting
// in b, foo: foo
// a done
// in a, setTimeout bar: 2

** Dynamic

The ES6 module will be statically analyzed during compile, which takes precedence over other content within the module, so we cannot write code like the following:

1
2
3
4
5
6
7
8
if(some condition) {
import a from './a';
}else {
import b from './b';
}

// or
import a from (str + 'b');

Because of static analysis during compile, we cannot use conditional statements or concatenate string modules, because these are results that need to be determined at runtime. ES6 modules are not allowed, so the dynamic introduction of import () came into being.

Import () allows you to dynamically import ES6 modules at runtime. You may also think of the syntax requirement.ensure, but their purposes are very different.

The emergence of requirem.ensure is a product of webpack. It is because the browser needs an asynchronous mechanism that can be used to load modules asynchronously, thereby reducing the size of the initial loaded file, so requirem.ensure is useless at the server level. Because there is no asynchronous loading of modules at the server level, the modules can be loaded synchronously to meet the usage scenarios. CommonJS modules can confirm module loading at runtime.

  • and import () is different, it is mainly to solve the ES6 module can not determine the reference relationship of the module at runtime, so it is necessary to introduce import ()

Let’s first look at its usage:

  1. Dynamic import () provides a Promise-based API
  2. Dynamic import () can be used anywhere in the script
  3. import () accepts string literals, you can construct specifiers according to your needs

Here’s a simple usage example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// a.js
const str = './b';
const flag = true;
if(flag) {
import('./b').then(({foo}) => {
console.log(foo);
})
}
import(str).then(({foo}) => {
console.log(foo);
})

// b.js
export const foo = 'foo';

// babel-node a.js
//Execution result
// foo
// foo

Of course, if the use of import () on the browser side will become more extensive, such as asynchronous loading of modules on demand, then it is similar to the requirement.ensure function.

Under what circumstances can the two be mixed?

Node.js

Node.js requires ES6 modules to use the .mjs suffix file name. That is, as long as the’import ‘or’export’ command is used in the script file, the ‘.mjs’ suffix must be used. Node.js encounters a ‘.mjs’ file and considers it an ES6 module. Strict mode is enabled by default. It is not necessary to specify ‘“use strict”’ at the top of each module file.

If you do not want to change the suffix to ‘.mjs’, you can specify the’type ‘field as’module’ in the’package.json 'file of the project.

1
2
3
{
"type":
}

Once set, the JS scripts in this directory are interpreted using ES6 modules.

1
2
#
$

If you still want to use the CommonJS module at this time, you need to change the suffix of the CommonJS script to ‘.cjs’. If there is no’type ‘field, or if the’type’ field is’commonjs’, the ‘.js’ script will be interpreted as a CommonJS module.

Summarized in one sentence: ‘.mjs’ files are always loaded as ES6 modules, ‘.cjs’ files are always loaded as CommonJS modules, and the loading of ‘.js’ files depends on the setting of the’type ‘field in’package.json’.

Note that the ES6 module and CommonJS module should not be mixed as much as possible. The require command cannot load the .mjs file and will report an error. Only the import command can load the .mjs file. Conversely, the require command cannot be used in the .mjs file, and the import command must be used.

CommonJS

CommonJS 'require () ’ command cannot load ES6 modules and will report an error. You can only use the’import () 'method to load.

1
2
3
(async
await
})();

The above code can be run in the CommonJS module.

One reason’require () ‘does not support ES6 modules is that it is loaded synchronously, and ES6 modules can use the top-level’await’ command internally, which cannot be loaded synchronously.

ES6

The’import 'command of the ES6 module can load CommonJS modules, but only as a whole, not just a single output item.

1
2
3
4
5
//
import

//
import

This is because ES6 modules need to support static code analysis, while the output interface of CommonJS modules is module.exports, which is an object that cannot be statically analyzed, so it can only be loaded as a whole.

Loading a single output item can be written as follows.

1
2
import
const

Supports modules in two formats at the same time

It is also easy for a module to support both CommonJS and ES6 formats.

If the original module is in ES6 format, then you need to give an overall output interface, such as’export default obj ', so that CommonJS can be loaded with’import () '.

If the original module is in CommonJS format, a wrapper layer can be added.

1
2
import
export

The above code first inputs the CommonJS module as a whole, and then outputs the named interface as needed.

You can change the suffix of this file to ‘.mjs’, or put it in a subdirectory, and then put a separate’package.json ‘file in this subdirectory, specifying’ {type: “module”} '.

Another approach is to specify the respective load entry points for the two format modules in the exports field of the package.json file.

1
2
3
4
"exports":{


}

The above code specifies’require () ‘and’import’. Loading this module will automatically switch to a different entry file.

webpack

Webpack itself can support its own way to ‘_ _ webpack _ exports webpack _ require _ _’ to replace CommonJS, ESModule, etc.

Babel converts ES6 import and export into CommonJS, which means that the converted code does not have import and export.

We will open another blog to analyze the specific source code, and there is already a lot of content at present.