The difference between CommonJS and ES6 modules

What are CommonJS and ES6 Modules?

  • Introduction to CommonJS, ES6 Module, AMD, UMD

CommonJS and ES6 Modules

  1. Dynamic and static
  2. Value copy and dynamic mapping
  3. Circular dependencies

1. Dynamic and static

The fundamental difference between CommonJS and ES6 Modules is that the former resolves Module dependencies “dynamically” while the latter is “static”. By “dynamic,” we mean that module dependencies are established at runtime; “Static” is when module dependencies are established during code compilation

Take a look at an example of CommonJS:

// calculator.js
module.exports = {
    name: 'calculator'
}
// index.js
const name = require('./calculator.js').name
Copy the code

As we mentioned in the CommonJS section above, when module A loads Calculator.js (in the example above, index.js loads Calculator.js), it executes the code in B and returns its module.exports object as the return value of the require function.

  • And require’s module path can be specified dynamically, allowing an expression to be passed in, and we can even determine whether to load a module using an if statement.
  • Therefore, there is no way to determine explicit dependencies before CommonJS modules are executed, and module imports and exports occur at runtime.

For the same example, let’s see how ES6 Module is written:

// calculator.js
export const name = 'calculator'

// index.js
import { name } from './calculator.js'
Copy the code

ES6 Module import and export statements are declarative. It does not support the import path as an expression, and import and export statements must be in the top-level scope of the Module (e.g. not in an if statement). Therefore, we say that ES6 Module is a static Module structure, and Module dependencies can be analyzed in the compilation stage of ES6 code.

ES6 Module has the following advantages over CommonJS:

  1. Dead code detection and exclusion. (tree shaking)
    • We can use static analysis tools to detect which modules are not called. For example, when a utility class library is introduced, the project often uses only a portion of the component or interface, but it is possible to load its code in its entirety. Module code that is not called is never executed and is dead code. Through static analysis, these unused modules can be removed during packaging to reduce the volume of packaging resources.
  2. Module variable type check.
    • JavaScript is a dynamically typed language and does not check for type errors (such as a function call to a value of type string) before code is executed. The static Module structure of ES6 Modules helps ensure that the values or interface types passed between modules are correct.
  3. Compiler optimization
    • In dynamic Module systems such as CommonJS, no matter which way is adopted, the import is essentially an object, while ES6 Module supports direct import variables, reducing the reference level, program efficiency is higher.

2. Value copy and dynamic mapping

When importing a module, CommonJS gets a copy of the exported value; In the ES6 Module, it is a dynamic mapping of values, and the mapping is read-only.

This may be difficult to understand directly, but let’s first look at an example of what value copying is in CommonJS.

// calculator.js
var count = 0
module.exports = {
    count,
    add: function (a, b) {
        count += 1
        return a + b
    }
}

// index.js
var count = require('./calculator.js').count
var add = require('./calculator.js').add

console.log(count) // 0 (where count is a copy of the count value in calculator.js)
add(2.3)
console.log(count) // 0 (Calculator.js changes in variable values do not affect the copy values here)

count += 1
console.log(count) // 1 (copy value can be changed)
Copy the code

The count in calculator.js is a copy of the value of count in calculator.js, so when you call add, the value of count in calculator.js is changed, but the copy created when calculator.js is inducted will not be affected.

On the other hand, changes to imported values are allowed in CommonJS. We can change count and add in index.js to give them new values. Again, since they are copies of values, these operations do not affect calculator.js itself.

Let’s rewrite the above example using the ES6 Module:

// calculator.js
let count = 0
const add = function (a, b) {
    count += 1
    return a + b
}
export { count, add }

// index.js
import { count, add } from './calculator.js'

console.log(count) // 0 (reference to the count value in calculator.js)
add(2.3)
console.log(count) // 1 (this is a reference, so it matches the count value in Calculator.js)

SyntaxError: 'count' is read-only
Copy the code

The example above shows that the variables imported in the ES6 Module are dynamic mappings of the original values. The count in calculator.js is a real-time reflection of the count value in calculator.js. When we change the count value in calculator.js by calling the add function, the count value in calculator.js also changes.

  • We cannot change the variables imported by ES6 Module. This mapping relationship can be understood as a mirror, from which we can observe the original things in real time, but we cannot manipulate the images in the mirror.

3. Circular dependencies

Cyclic dependency means that module A depends on module B while module B depends on module A

  • In general, cyclic dependencies should be avoided in projects

The ES6 Module has features that allow it to better support circular dependencies, but it is up to the developer to ensure that the correct export values are set when imported values are used.


Refer to Webpack Practice: Getting Started, Advancing and Tuning (Ju Yuhao)