post-css-position

Awesome JavaScript Module Tutorial that Must be Learned in 2019

Overview

JavaScript module lets you develop complex applications easily, allowing dividing complicated functionalities into multiple files.

Historically, JavaScript had no module system, and it is impossible to split a large program into small files that depend on each other and assemble them in a simple way. Other languages ​​have this feature, such as Ruby require, Python import, and even CSS @import, but there is no support for JavaScript in this area, which creates a huge obstacle to the development of large, complex projects.

Prior to ES6, the community developed some module loading solutions, the most common of which are CommonJS and AMD. The former is for the server and the latter for the browser. At the language standard level, ES6 implements module feature and makes it quite simple to use. It can completely replace the CommonJS and AMD specifications and become a common module solution for both browsers and servers.

The design concept of the ES6 module is as static as possible, so that the dependencies of the module, as well as the variables of the input and output, can be determined at compile time. CommonJS and AMD modules can only determine these things at runtime. For example, the CommonJS module is an object, and you must look up the object properties when you type.

// CommonJS module
let { stat, exists, readFile } = require('fs');

// equivalent to
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

The essence of the above code is the loading of the fs module (that is, all methods of fs are loaded), generating an object ( _fs). And then we read 3 methods from this object. This type of loading is called “runtime loading” because the object is only available at runtime, resulting in no way to do “static optimization” at compile time.

The ES6 module is not an object, but explicitly specifies the output code through a export command and then enters it through a import command.

// ES6 Module
import { stat, exists, readFile } from 'fs';

The essence of the above code is to load 3 methods from the fs module. And the other methods do not load. This type of loading is called “compile-time loading” or static loading. I.e., ES6 can complete module loading at compile time, which is more efficient than the CommonJS module technique. Of course, this also leads to the inability to reference the ES6 module itself because it is not an object.

Static analysis is made possible because the ES6 module is loaded at compile time. With it, you can further broaden the syntax of JavaScript, such as the introduction of macros and type systems, which can only be implemented by static analysis.

In addition to the various benefits of static loading, the ES6 module has the following benefits.

  • The UMD module format is no longer needed, and the ES6 module format will be supported by both the server and the browser in the future. At present, this has actually been achieved through various tool libraries.
  • In the future, the browser’s new API will be available in a module format, no longer having to be a global variable or an navigator object’s properties.
  • Objects are no longer needed as namespaces (such as the Math object), and in the future these functions can be provided through modules.

We first describe the syntax of the ES6 module and then describe how to load the ES6 module in the browser and Node.

Strict mode

The ES6 module automatically adopts strict mode, whether or not you add it to the module header "use strict";.

The strict mode mainly has the following restrictions.

  • Variables must be declared before use.
  • The parameters of the function cannot have the same name attribute, otherwise an error is reported.
  • Cannot use the with statement.
  • Cannot assign a value to a read-only property, otherwise an error is reported.
  • Cannot use the prefix 0 to indicate an octal number, otherwise an error is reported.
  • Cannot delete attributes that cannot be deleted, otherwise an error is reported.
  • Cannot delete variables using delete prop, will report an error. can only delete attributes through delete obj[prop].
  • The eval function does not introduce variables in its outer scope.
  • arguments can’t be reassigned.
  • arguments does not automatically reflect changes in function parameters.
  • Cannot use arguments.callee.
  • Cannot used arguments.caller.
  • Prohibit the this pointer pointing to global objects
  • Cannot use fn.caller and fn.arguments get the stack of function calls.
  • Added reserved words (such as protectedstatic and interface).

The modules must follow these restrictions. You can refer Strict Mode in MDN for more detail.

Pay special attention to the this restriction. In a ES6 module, the top-level this pointer points to undefined, which should not be used in the top-level code.

Export command

The module function is mainly composed of two commands: export and import. The export command is used to specify the external interface of the module, and the import command is used to input the functions provided by other modules.

A module is a separate file. All variables inside the file cannot be obtained externally. If you want the external to be able to read a variable inside the module, you must use the export keyword to output the variable. Below is a JavaScript file that uses the export command to output variables.

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

The above code is a profile.js file that holds a user’s information. ES6 treats it as a module with three variables outputted externally by the export command.

In addition, we can export multiple variables using one export.

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

The above code use curly braces to specify a set of variables to be output. It is equivalent to the former (placed directly before the var statement).

It should be preferred to use export {};. Because of this, you can see at a glance which variables are output at the end of the script. We will use this way in all later codes of this post.

In addition to output variables, the export command can also output functions or classes.

export function multiply(x, y) {
  return x * y;
};

The above code outputs a function multiply to the outside.

Normally, the outputted variable has the original name, but can be renamed using the as keyword.

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

The above code uses the as keyword to rename the external interface of the function v1 and the function v2. After renaming, you can output v2 twice with a different name.

It is important to note that the export command specifies the external interface and must have a one-to-one correspondence with the variables inside the module.

// Error
export 1;

// Error
var m = 1;
export m;

Both of the above methods will report an error because there is no external interface. The first type of writing directly outputs 1, the second type of writing passes the variable m, or directly outputs 1. Note that 1 just a value, not an interface. The correct way of writing is as follows.

// Method one
export var m = 1;

// Method two
var m = 1;
export {m};

// Method three
var n = 1;
export {n as m};

The above three ways are correct. Other scripts can take value 1 ​​through the interface m. The essence is that a one-to-one correspondence is established between the interface name and the internal variables of the module.

Similarly, the output of functions and classes must also follow this way of writing.

// Error
function f() {}
export f;

// Correct
export function f() {};

// Correct
function f() {}
export {f};

In addition, the interface output by the export statement has a dynamic binding relationship with the corresponding value, that is, the real-time value of the module can be obtained through the interface.

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

The above code output a variable foo. The value bar becomes baz after 500 milliseconds.

This is completely different from the CommonJS specification. The CommonJS module outputs a cache of values, and there is no dynamic update. See the section “Module Loading Implementation” below.

Finally, the export command can appear anywhere in a module, as long as it is at the top-level of the module. If it is in the block-level scope, an error will be reported, as the import command in the next section . This is because in the condition code block, there is no way to do static optimization, which violates the original intention of the ES6 module.

function foo() {
  export default 'bar' // SyntaxError
}
foo()

In the above code, the export statement is placed in the function, and an error is thrown.

Import command

After an external interface of a module is defined using the export command, other JS files can load the interface by the import command.

// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

The import command for the above code is used to load the profile.js file and obtain variables from it. The import command accepts a pair of curly braces that specify the names of the variables to be imported from other modules. The variable name inside the braces must be the same as the name of the external interface being imported.

If you want to re-name a variable, use the as keyword to rename the imported variable.

import { lastName as surname } from './profile.js';

The variables obtained by the import command are read-only because it is essentially an input interface. In other words, it is not allowed to rewrite the interface in the script that loads the module.

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

a = {}; // Syntax Error : 'a' is read-only;

In the above code, the script loads a variable a. And it will report an error if a is reassigned, because it is a read-only interface. However, if a is an object, reassigning properties of a is allowed.

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

a.foo = 'hello'; // This is allowed.

In the above code, the properties of a can be successfully rewritten, and other modules can also read the modified values. However, this kind of writing is difficult to check. It is recommended that all variables imported are treated as completely read-only. We should not change their properties.

The location of the specified module file can be either a relative path or an absolute path, and the .js suffix can be omitted. If it’s just a module name, without a path, then you must have a configuration file that tells the JavaScript engine where the module is.

import { myMethod } from 'util';

In the above code, util is the module file name. Since there is no path, it must be configured to tell the engine how to get the module. We will cover this topic later.

Note that the import command has a boost effect and will be promoted to the head of the entire module, first executed.

foo();

import { foo } from 'my_module';

The above code will not report an error because the import execution is earlier than the invocation of foo. The result of this behavior is that the import command is executed during the compilation phase, before the code runs.

Because import is static execution, you can’t use expressions and variables, which are the syntax structures that only get results at runtime.

// Error
import { 'f' + 'oo' } from 'my_module';

// Error
let module = 'my_module';
import { foo } from module;

// Error
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

The above three methods will report errors because they use expressions, variables, and if structure. The static analysis phase is unable to get values of these statements.

Finally, the import statement executes the loaded module. So we can import a module in this way:

import 'lodash';

The above code only executes the lodash module, but does not import any value.

If the same import statement is repeated multiple times , it will only be executed once.

import 'lodash';
import 'lodash';

The above code is loaded twice lodash, but only executed once.

import { foo } from 'my_module';
import { bar } from 'my_module';

// Equivalent to
import { foo, bar } from 'my_module';

The above code, though foo and bar are loaded in two statements, but they correspond to the same my_module instance. That is, the import statement follows a Singleton pattern.

Overall Loading of a Module

In addition to importing some variables from a module, you can also load the whole module, using the asterisk(*) symbol.

Below is a circle.js file that outputs two methods area and circumference.

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

Now load this module.

// main.js

import { area, circumference } from './circle';

console.log('Area of the circle:' + area(4));
console.log('Circumference of the circle:' + circumference(14));

The above is to specify the method to be loaded one by one, the overall loading is written as follows.

import * as circle from './circle';

console.log('Area of the circle:' + circle.area(4));
console.log('Circumference of the circle:' + circle.circumference(14));

Note that the object in which the module is loaded as a whole (in the above example circle) should be statically configurable, so runtime changes are not allowed. The following notation is not allowed.

import * as circle from './circle';

// The following two lines are incorrect.
circle.foo = 'hello';
circle.area = function () {};

Export Default Command

As you can see from the previous example, when using the import command, the user needs to know variable names or function names to be loaded. However, users definitely want to get started quickly, and may not be willing to read the documentation to understand what properties and methods that the module provided.

Use the export default command to specify the default output for a module.

// export-default.js
export default function () {
  console.log('foo');
}

The above code is a module file export-default.js whose default output is a function.

When other modules load the module, the import command can assign an arbitrary name to the anonymous function.

// import-default.js
import customName from './export-default';
customName(); // 'foo'

You don’t need to know the function name of the module’s output. It should be noted that braces are not used in the import command.

The export default command is also available for non-anonymous functions.

// export-default.js
export default function foo() {
  console.log('foo');
}

// Or

function foo() {
  console.log('foo');
}

export default foo;

In the above code, the function name foo is invalid outside the module. When loaded, it is treated as an anonymous function.

Compare the default output with the normal output below.

// Group one
export default function crc32() { // output
  // ...
}

import crc32 from 'crc32'; // input

// Group two
export function crc32() { // output
  // ...
};

import {crc32} from 'crc32'; // input

The two groups of codes above, when the first group uses default, the corresponding import statement does not need to use braces. When the second group does not use default, the corresponding import statement needs to use braces.

The export default command is used to specify the default output of the module. Obviously, a module can only have one default output, so the export default command can only be used once. Therefore, there is no need to add the parentheses in the import command, because the importation will match the only export default command.

Essentially, with export default, you output a default variable or method, and the system allows you to take any name for it. Therefore, the following is effective.

// modules.js
function add(x, y) {
  return x * y;
}

export default add;
// equivalent to
// export {add as default};

// app.js
import foo from 'modules';
// equivalent to
// import { default as foo } from 'modules';

Because the export default command actually outputs a default variable, so it cannot used with a variable declaration statement.

// Correct
export var a = 1;

// Correct
var a = 1;
export default a;

// Error
export default var a = 1;

In the above code, export default a means to assign the value of a to the variable default. Therefore, the last method of writing will give an error.

Similarly, because the essence of the command export default is to assign a value to the default variable, you can write a value directly after export default.

// Correct
export default 42;

// Error
export 42;

In the above code, the latter sentence is incorrect because the external interface is not specified, and the previous sentence specifies the external interface default.

With the export default command, the importation of a module is very intuitive. Take the lodash module as an example.

import _ from 'lodash';

If you want to obtain the default object and other interfaces in a single import statement, you can write the code as the following.

import _, { each, forEach } from 'lodash';

The export statement corresponding to the above code is as follows.

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };

In the above code, the each interface is exposed. The default interface points to an anonymous function. The forEach interface points to each.

export default can also be used to output classes.

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

The Combination of Export and Import

If you are in a module, first input and then output the same module. The import statement can be written together with the export statement.

export { foo, bar } from 'my_module';

// equivalent to
import { foo, bar } from 'my_module';
export { foo, bar };

As shown in the above code, export and import statements may be combined into a single line. However, it should be noted that foo and bar actually do not be imported into the current module. It is equivalent to forwarding the two interfaces to the outside.

Chaning module’s interface name and outputting the whole module can also use the same way.

// Changing name
export { foo as myFoo } from 'my_module';

// Outputting the whole module
export * from 'my_module';

The default interface is written as follows.

export { default } from 'foo';

The name interface is changed to the default interface as follows.

export { es6 as default } from './someModule';

// equivalent to
import { es6 } from './someModule';
export default es6;

Similarly, the default interface can be renamed to a named interface.

export { default as es6 } from './someModule';

The following three import statements have no corresponding compound writing.

import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";

There is a proposal, that proposed these three compound writing methods as following.

export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";

Module Inheritance

Modules can also be inherited.

Suppose there is a circleplus module that inherits the circle module.

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

In the above code, export * means output all interfaces of the circle module. Note that the export * command ignores the circle module’s default method. Then, the above code outputs the custom e variable and a default method.

At this time, you can also rename attributes or methods of circle and output them.

// circleplus.js

export { area as circleArea } from 'circle';

The above code indicates that only the area method of the circle module is outputted and its name is changed to circleArea.

The above module is loaded as follows.

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

The import exp representation loads the default method of the module circleplus as a expmethod.

Cross-Module Constant

The const declared constants are only valid in the current code block. If you want to set a constant across modules (that is, across multiple files), or a value to be shared by multiple modules, you can use the following notation.

// constants.js module
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js module
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js module
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

If you want to use a lot of constants, you can create a special constants directory, write various constants in different files, and save them in this directory.

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

Then, merge the constants output from these files in index.js.

// constants/index.js
export {db} from './db';
export {users} from './users';

When you use constants, you can load them directly .

// script.js
import {db, users} from './constants/index';

Import()

Introduction

As mentioned earlier, the import command is statically parsed by the JavaScript engine and executed before other statements in the module (It is more appropriate to call it binding). Therefore, the following code will report an error.

// Error
if (x === 2) {
  import MyModual from './myModual';
}

In the above code, the engine processes import statements at compile time. The if statement will not be parsed or executed at that time, so the import statement is meaningless in the if code block. A syntax error will be reported instead of a execution error.

The import and export command can only be at the top level of the module, not within a code block (for example, in a if code block, or in a function).

This design, while helping the compiler to improve efficiency, also makes it impossible to load modules at runtime. Syntactically, conditional loading is not possible. It is difficult to replace Node’s require with import. Because require loads modules at runtime. The import command cannot provide the dynamic loading feature.

const path = './' + fileName;
const myModual = require(path);

The above statement is dynamic loading, which module is loaded in the end, only known at runtime. The import command can’t do this.

Therefore, there is a proposal that introduces a import() function to accomplish the dynamic loading feature.

Module Loading Implementation

We have introduced the syntax of JavaScript module. This chapter describes how to load the ES6 module in the browser and Node, as well as some problems that are often encountered in actual development (such as loop loading).

Browser Side JavaScript Module Loading

Traditional Method

In an HTML page, the browser loads the JavaScript script through the <script> tag.

<! - Script embedded in the page ->
<script type="application/javascript">
  // module code
</script>

<!-- External script -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

In the above code, since the default language of the browser script is JavaScript, type="application/javascript" can be omitted.

By default, the browser synchronously loads JavaScript scripts, i.e., the rendering engine will stop when it encounters the <script> tag, and wait until the script is executed before continuing to render. If it is an external script, you must also include the time for downloading the script.

If the script is very large, the download and execution time will be very long, so the browser is blocked, the user will feel that the browser is “froze”. As a result, there is no response. This is obviously a very bad experience, so the browser allows the script to be loaded asynchronously. Here are the two asynchronous loading syntax.

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

In the above code, the <script> tag opens the defer or the async property, thus the script will be loaded asynchronously. When the rendering engine encounters this line of code, it first starts download the external script in background, and then directly execute the followed statements.

The difference of defer and async is:

  • defer: The entire page is rendered in memory: DOM structure is completely generated, as well as other script execution is complete. Then the script will be executed (once it is downloaded).
  •  async: As long as the script is downloaded, the rendering engine will break rendering to execute the script first, and then continue the rendering process.

In a word, defer is “rendering and then executing”; async is “executed after downloading”.

In addition, if there are multiple defer scripts, they will be loaded in the order in which they appear in the page, and multiple async scripts cannot guarantee the loading order.

Loading Rule

The browser loads the ES6 module using the <script> tag, but adds the type="module" attribute.

<script type="module" src="./foo.js"></script>

The above code inserts a module into the web page. Since the type property is set to module, the browser knows that this is an ES6 module.

The browser loads ES6 modules asynchronously, without blocking the rendering phase. After the entire page is rendered completely, the browser executes the script module. It is equivalent to the open the <script>‘s defer attribute.

<script type="module" src="./foo.js"></script>
<!-- equivalent to -->
<script type="module" src="./foo.js" defer></script>

If there are multiple pages <script type="module">, they will be loaded and executed in the order in which they appear.

The async property of the tag <script> can also be opened, and as soon as the loading is complete, the rendering engine will interrupt the rendering and execute the script immediately. After the execution is completed, the rendering is resumed.

<script type="module" src="./foo.js" async></script>

Once the async property is set, those scripts are not executed in the order in which they appear in the page, but modules are executed as soon as modules are loaded.

ES6 module allows to be embedded in Web pages directly. The behavior is exactly the same as loading external scripts.

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

For external module scripts (in the above example foo.js), there are a few points to note.

  • The code is run in the scope of the module, not in the global scope. The top-level variable inside the module is not visible externally.
  • Module scripts automatically adopt strict mode, with or without the declaration use strict.
  • Among the modules, you can use the import command to load other modules (the .js suffix cannot be omitted, you need to provide an absolute URL or a relative URL), or you can use the export command to output an interface.
  • Among the modules, the top-level this keyword returned undefined instead of pointing to the window object. In other words, using this at the top-level of the module is meaningless.
  • The same module will only be executed once if it is loaded multiple times.

Below is an example module.

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

You can detect if the current code is in the ES6 module using the top-level this.

const isNotModuleScript = this !== undefined;

Differences between ES6 Modules and CommonJS Modules

Before discussing NodeJS’s module loading mechanism, you must first understand that the ES6 module is completely different from the CommonJS module.

They have two major differences.

  • The CommonJS module outputs a copy of the value, and the ES6 module outputs a reference to the value.
  • The CommonJS module is runtime loaded and the ES6 module is a compile-time output interface.

The second difference is because CommonJS loads an object (the module.exports attribute) that is generated only after the script has finished running. The ES6 module is not an object. Its external interface is just a static definition, which is generated during the static parsing phase.

The following code highlights the first difference.

The CommonJS module outputs a copy of a value. Once the value is output, changes inside the module will not affect this value. Please see the example of a module file below.

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

The above code outputs a variable counter and a method incCounter to increase this variable. Then, main.js loads this module.

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

The above code shows that after the module is loaded, its internal change will not affect the variable mod.counter. Because mod.counter is a primitive type of value that will be cached. You can write a function to get the internally changed value.

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

In the above code, the output counter is actually a function. Now, in main.js, you can read the changes of the internal variables correctly .

$ node main.js
3
4

The ES6 module operates differently than CommonJS. When the JS engine statically analyzes a script, it encounters a module load command import and generates a read-only reference. Wait until the script is actually executed, and then according to the read-only reference, go to the loaded module to take the value. In other words, ES6’s import is a bit like the “symbolic connection” of Unix systems. If the original value changes, the loaded value will also change. Therefore, the ES6 module is a dynamic reference and does not cache values. The variables in the module are bound to the module they are in.

Still give the above example.

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

The above code shows that the variable counter imported by the ES6 module is alive and fully reflects the changes inside the module lib.js.

Give another example that appears in the export section.

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

In the above code, the variable foo is equal to bar just after loading the script m1.js. After 500 milliseconds, it becomes baz.

Let us see if this change can be read correctly.

$ babel-node m2.js

bar
baz

The above code shows that the ES6 module does not cache the results of the run, but dynamically takes the value of the loaded module, and the variable is always bound to the module it is in.

Since the module variable of the ES6 input is just a “symbolic connection”, this variable is read-only and re-assigning it will report an error.

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

In the above code, main.js obtains the variable obj from the lib.js. You can add/modify attributes of obj. But reassigning obj will report an error. Because the variable obj is read-only and can not be reassigned.

Finally, through the export interface, the output is the same value. Different scripts will get the same instance.

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

The script mod.js outputs an instance of C. Different scripts load this module and get the same instance.

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

Execute main.js will obtain 1.

$ babel-node main.js
1

This proves that x.js and y.js loaded the same instance.

Loading ES6 Module in NodeJS

Overview

Node’s handling of the ES6 module is cumbersome because it has its own CommonJS module format and is not compatible with the ES6 module format. The current solution is to separate the two, and the ES6 module and CommonJS use their own loading scheme.

Node requires the ES6 module to have a .mjs suffix filename. In other words, when using import or export commands, you must use the .mjs suffix name. The require command can’t load the .mjs file, it will report an error. Only the import command can load the .mjs file. In turn, the command require cannot be used in the .mjs file.

Currently, this feature is still in the experimental phase. Install Node v8.5.0 or above and use the --experimental-modules option to open this feature.

$ node --experimental-modules my-app.mjs

In order to be the same as the browser’s loading rules, Node’s .mjs files support URL paths.

import './foo?query=1'; // loading with a parameter

In the above code, the script path takes a parameter ?query=1. Node interprets it according to the URL rules. The same script will be loaded multiple times and saved as a different cache as long as the parameters are different. For this reason, as long as the file name contains special characters such as :%#? and so on, it is best to escape these characters.

Currently, the import command of Node supports loading local modules (i.e., the file: protocol) and do not support loading remote modules.

If the module name does not contain a path, the import command will go to the node_modules directory to find this module.

import 'baz';
import 'abc/123';

If the module name contains a path, the import command will follow the path to find the script file for that name.

import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';

If the script file omits the suffix name, for example import './foo', Node will try four suffixes in order: ./foo.mjs./foo.js./foo.json and ./foo.node. If all these scripts do not exist, Node will load the mainscript specified by ./foo/package.json. If there is no main field, then it will try to load ./foo/index.mjs./foo/index.js./foo/index.json and ./foo/index.node sequentially. If the above four files still do not exist, an error will be thrown.

Finally, Node’s import command is asynchronous, which is the same as the browser’s handling.

Internal Variable

ES6 modules should be generic, and the same module can be used in both browser and server environments without modification. To achieve this goal, Node stipulates that some of the internal variables specific to the CommonJS module cannot be used in the ES6 module.

First of all, it is the this keyword. Among the ES6 modules, the top level this points to undefined; the top level this of the CommonJS module points to the current module, which is a major difference between the two.

Second, the following top-level variables do not exist in the ES6 module.

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

Loading CommonJS Module in ES6 Module

The output of the CommonJS module is defined in the property module.exports. The Node import command loads the CommonJS module, and automatically takes the module.exports property as the default output of the module, which is equivalent to export default xxx.

Below is a CommonJS module.

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// equivalent to
export default {
  foo: 'hello',
  bar: 'world'
};

The import command loads the above module and module.exports is treated as the default output, i.e., the import command actually takes such an object { default: module.exports } as its input.

Therefore, there are three ways to get the CommonJS module’s module.exports.

// Way one
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// Way two
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

// Way three
import * as baz from './a';
// baz = {
//   get default() {return module.exports;},
//   get foo() {return this.default.foo}.bind(baz),
//   get bar() {return this.default.bar}.bind(baz)
// }

The third way written above, we can get module.exports through baz.default.

Here are some examples.

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = { default:null };

In es.js, to obtain module.exports, you should use the second method through bar.default.

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

In the above code, bar itself is an object. It can not be called as a function. You can only invoke it through bar.default.

Loading ES6 Module in CommonJS Module

To load ES6 modules in a CommonJS module, you should use import() function and can not use the require command. All output interfaces of the ES6 module become properties of the input object.

// es.mjs
let foo = { bar: 'my-default' };
export default foo;

// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
//   get default() {
//     ...
//   }
// }
console.log(es_namespace.default);
// { bar:'my-default' }

In the above code, the default interface becomes an es_namespace.default attribute.

Here’s another example.

// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

Circular Dependency

“Cyclic dependence” means that the execution of a depends on the script b, and the execution of b depends on a.

// a.js
var b = require('b');

// b.js
var a = require('a');

In general, “Cyclic dependence” means that there is a strong coupling. If it is not handled well, it may also cause recursive loading, making the program unexecutable, so it should be avoided.

But in fact, this is very difficult to avoid, especially for large projects with complex dependencies. This means that the module loading mechanism must consider the case of “Cyclic dependence”.

For the JavaScript language, the two most common module formats, CommonJS and ES6, handle the “Cyclic dependence” method differently, and the returned results are different.

The Loading Principle of the CommonJS Module

We first introduce the loading principle of CommonJS module.

A module of CommonJS is a script file. The first time the require command loads the script, it executes the entire script and then generates an object in memory.

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

The above code is an object generated after Node loading of the module internally. The id property of the object is the module name. The exports property is the interface of the module output, and the loaded property is a Boolean value indicating whether the script of the module is executed. There are many other properties that are omitted here.

When you need to use this module in the future, you will take values from the exports property. Even if the require command is executed again, the module will not be executed again. The cached values are obtained. That is, the CommonJS module will only run once on the first load, no matter how many times it is loaded. If it is loaded later, it will return the result of the first run unless the system cache is manually cleared.

Cyclic Loading of CommonJS Modules

An important feature of the CommonJS module is load-time execution, which means that when the script code is required, it will be executed. Once a module is “Cyclic loading”, only the part that has been executed is output, and the part that has not been executed is not output.

Let’s take a look at the examples in the Node official documentation . The script file a.js code is as follows.

exports.done = false;
var b = require('./b.js');
console.log('In a.js, b.done = %j', b.done);
exports.done = true;
console.log('a.js done');

In the above code, the a.js script first outputs a done variable and then loads another script file b.js. Note that the a.js code stops here and waits for b.js execution to complete.

Look at the code b.js again.

exports.done = false;
var a = require('./a.js');
console.log('In b.js, a.done = %j', a.done);
exports.done = true;
console.log('b.js done');

In the above code, when b.js is executed to the second line, a.js will be loaded. At this time, “Cyclic loading” occurs. The system will get the exports attribute of a.js to obtain the corresponding object of the module. However, a.js has not finished execution, the exports can only retrieve the part that has been executed, not the last value.

The only part of a.js that has been executed is one line.

exports.done = false;

So, for b.js, it obtains a variable done from a.js with the value false.

Then, b.js goes on and wait until all the execution is complete, and then returns the execution right to a.js. So, a.js goes on until the execution is complete. We write a script main.js to verify the process.

var a = require('./a.js');
var b = require('./b.js');
console.log('In main.js, a.done=%j, b.done=%j', a.done, b.done);

Execution main.js, the results are as follows.

$ node main.js

In b.js, a.done = false
b.js done
In a.js, b.done = true
a.js done
In main.js, a.done=true, b.done=true

The above code proves two things. First, in b.js, the execution of a.js was not completed and only the first line was executed. Second, when main.js executed to the second line, b.js will not be executed again. The result of b.js is obtained from the cache , that is, its fourth line.

exports.done = true;

In summary, CommonJS inputs a copy of the output value, not a reference.

In addition, since the CommonJS module encounters a cyclic load, it returns the value of the currently executed part, not the value after the code is fully executed, and there may be differences between the two. Therefore, you must be very careful when importing variables.

var a = require('a'); // Safe way of writing
var foo = require('a').foo; // Dangerous writing

exports.good = function (arg) {
  return a.foo('good', arg); // Using the latest value of a.foo
};

exports.bad = function (arg) {
  return foo('bad', arg); // Using a partial load value
};

In the above code, if a cyclic load occurs, the value require('a').foo is likely to be overwritten later, and require('a')will be safer to use it instead.

Cyclic Loading of ES6 modules

ES6’s processing of “Cyclic loading” is fundamentally different from CommonJS. ES6 modules are dynamic references. If you use import to load variables from a module (e.g., import foo from 'foo'), those variables will not be cached, but instead become a reference to the loaded module. The developer needs to ensure that the value can be obtained when the value is fetched.

Please see the example below.

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

In the above code, a.mjs loads b.mjsb.mjs loads a.mjs, and form a cyclic load. Execution of a.mjs gives the following results.

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

In the above code, an error will be reported after execution , and the foo variable is undefined. Why?

Let’s take a look at how the ES6 cyclic loading process is handled. First, in the execution of a.mjs, the engine finds that it loads b.mjs, so it will execute b.mjs first and then execute a.mjs. Then, when b.mjs is executed , it is known that it has obtained the foo interface from a.mjs. Thus a.mjs is not executed at this time. Instead, it thinks that this interface already exists and continues to execute. When executed to the third line console.log(foo), the engine found that this interface was not defined at all, so an error is reported.

The way to solve this problem is to let the b.mjs has foo definition during the execution. This can be solved by writing a foo function.

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

At this point, you can get the expected results.

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

This is because the function has an promotion feature. When it is executed import {bar} from './b', the function foois already defined, so no error is reported when loading b.mjs. This also means that if you rewrite the function foo as a function expression, you will get an error.

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};

The fourth line of the above code, changed to a function expression, will not have a promotion, and execution will report an error.

Closing Words

WOW! You have read this long article! You are on the way to becoming a JavaScript expert.

Below are some of the previous JavaScript articles:

If you encounter any issue, please leave us a comment.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *