Front-end modularization is the first and important step of front-end engineering. Whether you use React, Vue, or Nodejs, modularity is essential. There are many modularity specifications, and the CommonJS and ES6 specifications are the most commonly used, so let’s take a closer look at these two specifications and the differences between them.

I am participating in the Nuggets 2020 Most Popular Author list, and I look forward to your valuable vote

This article was first published in the public number [front-end one read], more exciting content please pay attention to the latest news of the public number.

CommonJS

The CommonJS specification is a way to load modules synchronously, meaning that you can’t perform subsequent operations until the module has finished loading. Because Nodejs is primarily used for server-side programming, and module files usually already exist on the local hard disk and can be loaded quickly, the CommonJS specification for synchronous loading of modules is applicable.

An overview of the

The CommonJS specification states that each JS file is a module and has its own scope. Variables, functions, and so on defined in a module are private variables and not visible to other files.

// number.js
let num = 1
function add(x) {
    return num + x
}
Copy the code

In number.js above, the num variable and the add function are private to the current file and cannot be accessed by other files. At the same time, CommonJS specifies that each module has a module variable inside, representing the current module; This variable is an object whose exports property (module.exports) provides an interface to exported modules.

// number.js
let num = 1
function add(x) {
    return num + x
}
module.exports.num = num
module.exports.add = add
Copy the code

So that the private variables we define can be accessed externally; When you load a module, you load the module.exports property of that module.

module

The module variable represents the current module. Let’s print it to see what information it contains:

//temp.js
require('./a.js')
console.log(module)

Module {
  id: '. '.path: 'D:\\demo'.exports: {},
  parent: null.filename: 'D:\\demo\\temp.js'.loaded: false.children: [{
    Module {
      id: 'D:\\demo\\a.js'.path: 'D:\\demo'.exports: {},
      parent: [Circular],
      filename: 'D:\\demo\\a.js'.loaded: true.children: [].paths: [Array]}}],paths: [
    'D:\\demo\\node_modules'.'D:\\projects\\mynodejs\\node_modules'.'D:\\projects\\node_modules'.'D:\\node_modules']}Copy the code

We found that it has the following properties:

  • Id: The module’s identifier, usually the module’s file name with an absolute path
  • Filename: indicates the filename of the module with an absolute path.
  • Loaded: Returns a Boolean value indicating whether the module has completed loading.
  • Parent: Returns an object representing the module that called the module.
  • Children: Returns an array representing other modules used by this module.
  • Exports: Objects exported by modules.
  • Path: indicates the directory name of the module.
  • Paths: Search path of a module.

If we call a module from the command line, such as Node Temp.js, the module is the top-level module, and its module.parent is null; If it is called in another module, such as require(‘temp.js’), then its module.parent is the module that called it.

Module. parent is deprecated in Nodejs 14.6, and is recommended to use require.main or module.children instead.

The module.parent value is the value of the module referenced via required. Null if is the entry of the currently running process. If the module is imported in a non-CommonJS format, such as REPL, or import, the value is undefined

exports

Exports can also be exported via the exports variable, which points to module.exports, so this implicitly adds a line of code to each module:

var exports = module.exports;
Copy the code

You can add properties to exports objects when exporting modules.

// number.js
let num = 1
function add(x) {
    return num + x
}
exports.num = num
exports.add = add
Copy the code

Note that you cannot point the exports variable directly to a value, as this breaks the link between exports and module.exports

// a.js
exports = 'a'
// main.js
var a = require('./a')
console.log(a)
/ / {}
Copy the code

Exports = module.exports; exports = module.exports; exports = module.

require

The basic function of require is to read and execute JS files and return module.exports objects exported by modules:

const number = require("./number.js")
console.log(number.num)
number.add()
Copy the code

If a module exports a function, it cannot be defined on an exports object:

// number.js
module.exports = function () {
  console.log("number")}// main.js
require("./number.js") ()Copy the code

In addition to being able to load modules as function calls, require itself as an object has the following properties:

  • Resolve: indicates the path of the module to be resolved.
  • Main:ModuleObject that represents the entry script loaded when the process starts.
  • Extensions: How to handle file extensions.
  • Cache: The object in which imported modules are cached.

Module cache

When we require the same module multiple times in a project, CommonJS does not execute the module file multiple times; Instead, the module is cached on the first load; When the module is loaded later, it is read directly from the cache:

//number.js
console.log('run number.js')
module.exports =  {
    num: 1
}

//main.js
let number1 = require("./number");

let number2 = require("./number");
number2.num = 2

let number3 = require("./number");
console.log(number3)

// run number.js
// { num: 2 } 
Copy the code

We require loading the number module several times, but we only print it out internally once; The second load also changes the value of the internal variable, and the third load changes the value of the internal variable, which proves that the later require reads the cache.

In the previous require, we introduced all the following properties, and found that there is a cache property, which is used to cache modules.

{
    'D:\\demo\\main.js': Module {},
    'D:\\demo\\number.js': Module {}
}
Copy the code

Cache cache modules in the form of a path, we can delete a cached module by delete require.cache[modulePath]. Let’s rewrite the above code:

//number.js
console.log('run number.js')
module.exports =  {
    num: 1
}

//main.js
let number1 = require("./number");

let number2 = require("./number");
number2.num = 2

// Delete the cache
delete require.cache['D:\\demo\\number.js']

let number3 = require("./number");
console.log(number3)

// run number.js
// run number.js
// { num: 1 } 
Copy the code

It is obvious that the number module has been run twice, and the cache of the module has been cleared the second time the module has been loaded, so the num value read the third time is also the latest. We can also remove the cache for all modules through the object. keys loop:

Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})
Copy the code

Loading mechanism

The CommonJS loading mechanism is that a module outputs a copy of a value; For primitive data type output, it is copy, for complex data type, it is shallow copy, let’s look at an example:

// number.js
let num = 1
function add() {
    num++
}
module.exports.num = num
module.exports.add = add

// main.js
var number = require('./number')
/ / 1
console.log(number.num)

number.add()
/ / 1
console.log(number.num)

number.num = 3
/ / 3
console.log(number.num)
Copy the code

Since CommonJS is a copy of a value, once a module outputs a value, changes within the module do not affect that value; Therefore, the number variable in main.js has no reference relationship with number.js. Even if we call the add function inside the module to change the value, it will not affect the value. Instead, we can edit the value any way we want after output.

The require feature can also be understood as putting modules into self-executing functions:

var number = (function(){
    let num = 1
    function add() {
        num++
    }
    return {
        num,
        add,
    }
})()
/ / 1
console.log(number.num)

number.add()
/ / 1
console.log(number.num)
Copy the code

For complex data types, since CommonJS is a shallow copy, if both scripts reference the same module, changes to that module will affect the other module:

// obj.js
var obj = {
    color: {
        list: ['red'.'yellow'.'blue']}}module.exports = obj

//a.js
var obj = require('./obj')
obj.color.list.push('green')
//{ color: { list: [ 'red', 'yellow', 'blue', 'green' ] } }
console.log(obj)

//b.js
var obj = require('./obj')
//{ color: { list: [ 'red', 'yellow', 'blue', 'green' ] } }
console.log(obj)

//main.js
require('./a')
require('./b')
Copy the code

In the above code, we reference a module for modification and reading through two scripts a.js and B. js; Note that because of the cache, B.js is already loaded as a module read from the cache.

The module’s module.exports property is returned as a value when require loads. We find that this loading process occurs during the runtime of the code, and there is no way to determine the dependencies of the modules before they are executed. This loading method is called runtime loading. Since the CommonJS runtime loads modules, we can even dynamically select a module to load with a judgment statement:

let num = 10;

if (num > 2) {
    var a = require("./a");
} else {
    var b = require("./b");
}

var moduleName = 'number.js'

var number = require(`. /${moduleName}`)
Copy the code

But because of this dynamic loading, there is no way to do static optimization at compile time.

Cyclic loading

Because of the caching mechanism, CommonJS modules can be looping between them without worrying about causing an infinite loop:

//a.js
exports.a = 1;
var b = require("./b");
console.log(b, "a.js");
exports.a = 2;


//b.js
exports.b = 11;
var a = require("./a");
console.log(a, "b.js");
exports.b = 22;


//main.js
const a = require("./a");
const b = require("./b");

console.log(a, "main a");
console.log(b, "main b");
Copy the code

In the above code, the logic seems to be very complicated, a.js loads b.js, and b.js loads a.js; But if we break it down one by one, it’s pretty simple.

  1. When main.js is loaded, it is found that module A is loaded. Read and store in cache
  2. Execute module A, export {a:1}; Module B is found to be loaded, read and cached
  3. Execute b module, export {b:11}; Module A was loaded again and the cache was read. At this time, module A only exported {a:1}.
  4. Module B is executed and {b:22} is exported.
  5. Return to module A, execute, export {a:2}
  6. Back to main.js, load the B module again and read the cache

So the final printed result:

{ a: 1 } b.js   
{ b: 22 } a.js  
{ a: 2 } main a 
{ b: 22 } main b
Copy the code

In particular, the console in the first module B, because module A has been loaded in the cache but has not completed execution, module A only exported the first {a:1}.

We found that cyclic loading, which is load-time; Once a module is looping, only the parts that have been executed are printed, and the parts that have not been executed are not.

ES6

Unlike dynamic loading in the CommonJS specification, ES6 modularity is designed to be as static as possible so that dependencies between modules can be determined at compile time. We talked about how Tree Shaking can optimize code using ES6 module static loading solutions.

export

Like CommonJS, the ES6 specification defines a JS file as an independent module. Variables inside the module are private and cannot be accessed by other modules. ES6 uses the export keyword to export a variable, function, or class:

export let num = 1
export function add(x) {
    return num + x
}
export class Person {}
Copy the code

Or we can export an object directly, which is equivalent:

let num = 1
function add(x) {
    return num + x
} 

class Person {}
export { num, add, Person }
Copy the code

When exporting an object, we can also rename the exported variable using the as keyword:

let num = 1
function add(x) {
    return num + x
} 

export {
    num as number,
    num as counter,
    add as addCount,
    add as addFunction
}
Copy the code

With the same name as, we have exported the variable several times. It should be noted that export stipulates that the exported interface is external, and a one-to-one correspondence relationship must be established with variables inside the module. Here are two incorrect ways to write:

// Error, a value, no interface provided
export 1;

// An error is reported and needs to be placed in braces
var m = 1;
export m;
Copy the code

import

After exporting the external interface of a module using export, other module files can load this interface using the import command:

import {
    number,
    counter,
    addCount,
    addFunction
} from "./number.js"
Copy the code

The above code loads variables from the number.js module, and the import command accepts a pair of curly braces specifying the names of variables imported from the module. The imported variable names must be the same as those of the external interface of the imported module.

As with the export command, we can rename the imported variable name using the as keyword:

import {
    number as num,
} from "./number.js"
console.log(num)
Copy the code

In addition to specifying the variable interface in the loading module, we can also use global loading by specifying an object with (*) on which all output values are loaded:

import * as number from "./number.js"
Copy the code

The import command is promoted to the head of the entire module, first executing:

console.log(num)
import {
    number as num,
} from "./number.js"
Copy the code

The above code does not report an error because the import takes precedence; Unlike require in the CommonJS specification, import is executed statically, so import cannot be in block-level scope and cannot use expressions and variables, which are syntactic constructs that can only be obtained at runtime:

/ / an error
let moduleName = './num'
import { num, add } from moduleName;


/ / an error
//SyntaxError: 'import' and 'export' may only appear at the top level 
let num = 10;
if (num > 2) {
    import a from "./a";
} else {
    import b from "./b";
}
Copy the code

export default

In the above code, when import and import export interface, we need to know the exact name of the external interface to get the corresponding value, which is quite troublesome. Sometimes we only have one interface to export. The ES6 specification provides export default to export by default:

//add.js
export default function (x, y) {
    return x + y;
};
//main.js
import add from './add'
console.log(add(2.4))
Copy the code

Since export default is the default export, this command can only be used once in a module, whereas the export interface can be exported multiple times:

/ / an error
//SyntaxError: Only one default export allowed per module.
//add.js
export default function (x, y) {
    return x + y;
};
export default function (x, y) {
    return x + y + 1;
};
Copy the code

Export default is a syntactic sugar that essentially assigns the following value to the default variable, so you can write a value after export default. But since it prints a default variable, it cannot be followed by a variable declaration statement:

/ / right
export default 10

/ / right
let num = 10
export default num

/ / an error
export default let num = 10
Copy the code

Since export default essentially exports the syntactic sugar of a default variable, we can also rewrite it through export:

//num.js
let num = 10;
export { num as default };
Copy the code

The above two codes are equivalent; When we import, we rename the default variable to the name we want, so the following two import codes are also equivalent:

import num from './num'
/ / equivalent
import { default as num } from './num'
Copy the code

In a module, there can be multiple export and only one export default, but both can exist at the same time:

//num.js
export let num1 = 1
export let num2 = 2
let defaultNum = 3
export default defaultNum

//main.js
import defaultNum, {
  num1,
  num2
} from './num'
Copy the code

Loading mechanism

In CommonJS we said that the output of a module is a copy of a value; ES6 output is the external interface, we rewrite the above code in CommonJS to understand the difference between the two:

//number.js
let num = 1

function add() {
  num++
}

export { num, add }

//main.js
import { num, add } from './number.js'

/ / 1
console.log(num)
add()
/ / 2
console.log(num)
Copy the code

We found that calling a function in the module affects the value of a variable in the module, which is completely different from CommonJS. Since the ES6 module only outputs an external interface, we can think of the interface as a reference, and the actual value is still in the module; And the reference is also a read-only reference, whether of a primitive or complex data type:

//obj.js
let num = 1
let list = [1.2]

export { num, list }

//main.js
import { num, list } from './obj.js'
//Error: "num" is read-only.
num = 3
//Error: "list" is read-only.
list = [3.4]
Copy the code

Import also caches imported modules. Repeating the import of the same module will only be executed once, so there is no code demonstration here.

A circular reference

ES6 modules also have circular references between modules, so let’s change the CommonJS code to look at:

//a.js
export let a1 = 1;
import { b1, b2 } from "./b";
console.log(b1, b2, "a.js");
export let a2 = 11;

//b.js
export let b1 = 2;
import { a1, a2 } from "./a";
console.log(a1, a2, "b.js");
export let b2 = 22;

//main.js
import { a1, a2 } from "./a";
import { b1, b2 } from "./b";
Copy the code

At first, we must take it for granted that b.js prints 1 and undefined, because a.js only loads the first export; However, after printing the result, both of them in B. js are undefined, because import has promotion effect.

The difference between summary

Using our comparison of CommonJS and ES6 specifications above, we can summarize the differences between the two:

  • The CommonJS module is run time loaded, and the ES6 module is compile time output interface
  • The CommonJS module prints a copy of a value, the ES6 module prints a reference to a value
  • CommonJS loads the whole module, meaning that all methods are loaded in. ES6 can load one of these methods individually
  • In the CommonJSthisPoint to the current modulemodule.exportsAnd in the ES6thisPointing to the undefined
  • CommonJS default is non-strict mode, ES6 modules automatically adopt strict mode

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles