Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

The syntax of ES6 Module


May 08, 2021 ES6


Table of contents


1. Overview

Historically, JavaScript has 模块 a module system, unable to split a large program into interdependent small files and put them together in simple ways. Other languages have this capability, such as Ruby's require, Python's import, or even CSS has @import, but JavaScript doesn't support this, which creates a huge barrier to developing large, complex projects.

Prior to ES6, the community developed a number of module loading scenarios, CommonJS AMD T he former is used 服务器 and the latter is used 浏览器 ES6 implements module functionality at the language standard level and is simple enough to replace the CommonJS and AMD specifications as a common module solution for browsers and servers.

The ES6 module is designed to be as static as possible so that the module's dependencies, as well as the input and output variables, can be determined at compile time. C ommonJS and AMD modules can only identify these things at runtime. For example, the CommonJS module is an object, and object properties must be found when entering.

  1. // CommonJS模块
  2. let { stat, exists, readfile } = require('fs');
  3. // 等同于
  4. let _fs = require('fs');
  5. let stat = _fs.stat;
  6. let exists = _fs.exists;
  7. let readfile = _fs.readfile;

The essence of the above code is to load the fs module as a whole (that is, all methods to load fs), generate an object (_fs), and then read three methods from above the object. This load is called a "run-time load" because only the runtime can get the object, making it impossible to do "static optimization" at compile time at all.

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

  1. // ES6模块
  2. import { stat, exists, readFile } from 'fs';

The essence of the above code is to load 3 methods from the fs module, while the other methods do not. T his load is “编译时加载” 静态加载 static loading, which means that ES6 can load modules at compile time, more efficiently than CommonJS modules. Of course, this also makes it impossible to reference the ES6 module itself because it is not an object.

Static analysis is possible because ES6 modules are loaded at compile time. With it, javaScript syntax can be further broadened, such as the introduction of macro and type system, functions that can only be achieved by static analysis.

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

  • UMD module formats are no longer required, and ES6 module formats will be supported by servers and browsers in the future. At present, through a variety of tool libraries, in fact, has done this.
  • In the future, the browser's new API will be available in module format and will no longer have to be made into a global variable or a property of the navigator object.
  • Objects are no longer needed as namespaces, such as Math objects, and these features can be provided through modules in the future.

This chapter describes the syntax of the ES6 module, and the next chapter describes how to load the ES6 module in a browser and Node.

2. Strict mode

The module of ES6 automatically uses strict mode, whether or not you add "use strict" to the head of the module;

Strict mode has the following limitations.

  • Variables must be declared before they can be used
  • The argument of the function cannot have a property with the same name, otherwise an error is reported
  • The with statement cannot be used
  • You cannot assign a value to a read-only property, otherwise an error is reported
  • You cannot use the prefix 0 to represent the octal number, otherwise an error is reported
  • You cannot delete non-deleteable properties, otherwise an error cannot be reported
  • The variable delete prop cannot be deleted, an error can be reported, and only the property delete global can be deleted.
  • eval does not introduce variables in its outer scope
  • eval and arguments cannot be re-assigned
  • arguments do not automatically reflect changes in function parameters
  • Arguments.callee cannot be used
  • Arguments.caller cannot be used
  • This is prohibited from pointing to a global object
  • You cannot use fn.caller and fn.arguments to get the stack of function calls
  • Added reserved words (e.g. protected, static, and interface)

These limitations must be observed by the module. Since strict mode was introduced by ES5 and is not part of ES6, see the relevant ES5 book, which is no longer detailed.

Of these, it is particularly important to note the limitations of this. In the ES6 module, the top-level this points to the undefined, i.e. this should not be used in the top-level code.

3. Export command

模块 module function consists 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 enter the functions provided by other modules.

A module is a separate file. A ll variables inside the file are not available externally. I f you want an external read of a variable inside a module, you must output the variable using the export keyword. Below is a JS file that uses the export command to output variables.

  1. // profile.js
  2. export var firstName = 'Michael';
  3. export var lastName = 'Jackson';
  4. export var year = 1958;

The code above is the profile .js file, which holds the user information. ES6 treats it as a module that outputs three variables externally with the export command.

There's another way to write export, in addition to it like this.

  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export { firstName, lastName, year };

The above code, after the export command, uses braces to specify a set of variables to output. I t is equivalent to the previous method of writing (placed directly before the var statement), but should be preferred. Because this allows you to see at a glance which variables are output at the end of the script.

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

  1. export function multiply(x, y) {
  2. return x * y;
  3. };

The above code outputs a function multiply.

Typically, the variable of the export output is the original name, but can be renamed using the as keyword.

  1. function v1() { ... }
  2. function v2() { ... }
  3. export {
  4. v1 as streamV1,
  5. v2 as streamV2,
  6. v2 as streamLatestVersion
  7. };

The above code uses the as keyword to rename the external interfaces of functions v1 and v2. After renaming, v2 can be output twice under a different name.

It is important to note that the export command provides for an external interface that must establish a one-to-one correspondence with the variables inside the module.

  1. // 报错
  2. export 1;
  3. // 报错
  4. var m = 1;
  5. export m;

Both of the above writings report errors because they do not provide an external interface. T he first write outputs 1 directly, the second writes through the variable m, or outputs 1 directly. 1 is just a value, not an interface. The correct way to write is this.

  1. // 写法一
  2. export var m = 1;
  3. // 写法二
  4. var m = 1;
  5. export {m};
  6. // 写法三
  7. var n = 1;
  8. export {n as m};

The above three writings are correct and provide for the external interface m. O ther scripts can get a value of 1 through this interface. The essence of them is that a one-to-one correspondence is established between the interface name and the variables inside the module.

Similarly, the output of function and class must be written in this way.

  1. // 报错
  2. function f() {}
  3. export f;
  4. // 正确
  5. export function f() {};
  6. // 正确
  7. function f() {}
  8. export {f};

In addition, the interface output of the export statement, its corresponding value is a dynamic binding relationship, that is, through the interface, you can get the real-time value inside the module.

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

The above code outputs the variable foo, with a value of bar, which becomes baz after 500 milliseconds.

This is completely different from the CommonJS specification. The CommonJS module outputs a cache of values, with no dynamic updates, as detailed in the "Loading Implementation of Module" section below.

Finally, the export command can appear anywhere in the module, as long as it is at the top level of the module. I f you are in a block-level scope, an error is reported, as is the import command in the next section. This is because in a conditional block of code, static optimization is not necessary, contrary to the design intent of the ES6 module.

  1. function foo() {
  2. export default 'bar' // SyntaxError
  3. }
  4. foo()

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

4. Import command

Once export interface of the module has been defined using the export command, other JS files can load the module through the import command.

  1. // main.js
  2. import { firstName, lastName, year } from './profile.js';
  3. function setName(element) {
  4. element.textContent = firstName + ' ' + lastName;
  5. }

The import command of the code above, which is used to load the profile .js file and enter variables from it. T he import command accepts a pair of braces that specify the name of the variable to import from another module. The name of the variable in braces must be the same as the name of the external interface .js the imported module (profile).

If you want to re-name the variable you entered, the import command renames the entered variable using the as keyword.

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

The variables entered by the import command are read-only because their essence is the input interface. That is, the interface is not allowed to be overwritten in the script that loads the module.

  1. import {a} from './xxx.js'
  2. a = {}; // Syntax Error : 'a' is read-only;

In the above code, the script loads the variable a and re-assigns it to an error because a is a read-only interface. However, if a is an object, rewriting a's properties is allowed.

  1. import {a} from './xxx.js'
  2. a.foo = 'hello'; // 合法操作

In the above code, the properties of a can be successfully overwrite, and other modules can read the overwrite values. However, this kind of writing is difficult to find out, it is recommended that all input variables, as completely read-only, do not easily change its properties.

From after import specifies the location of the module file, between a relative path and an absolute path, .js the suffix can be omitted. If it's just a module name with no path, you must have a profile that tells the JavaScript engine where the module is located.

  1. import {myMethod} from 'util';

In the code above, util is the module file name, because there is no path, must be configured to tell the engine how to get this module.

Note that the import command has a lift effect and is raised to the head of the entire module, first executed.

  1. foo();
  2. import { foo } from 'my_module';

The above code does not report errors because import executes earlier than foo calls. The essence of this behavior is that the import command is executed during the compilation phase, before the code runs.

Because import is executed statically, expressions and variables cannot be used, which are syntax structures that only get results at runtime.

  1. // 报错
  2. import { 'f' + 'oo' } from 'my_module';
  3. // 报错
  4. let module = 'my_module';
  5. import { foo } from module;
  6. // 报错
  7. if (x === 1) {
  8. import { foo } from 'module1';
  9. } else {
  10. import { foo } from 'module2';
  11. }

All three of the above writes report errors because they use expressions, variables, and if structures. During the static analysis phase, these syntaxes do not get value.

Finally, the import statement executes the loaded module, so you can write as follows.

  1. import 'lodash';

The above code only executes the lodash module, but does not enter any values.

If you repeat the same import statement more than once, you will execute it only once, not more than once.

  1. import 'lodash';
  2. import 'lodash';

The above code loads the lodash twice, but only once.

  1. import { foo } from 'my_module';
  2. import { bar } from 'my_module';
  3. // 等同于
  4. import { foo, bar } from 'my_module';

In the above code, although foo and bar are loaded in two statements, they correspond to the same my_module instance. That is, the import statement is singleton mode.

At this stage, the Require command for the CommonJS module, and the import command for the ES6 module can be written in the same module, but it is best not to do so. B ecause import is executed during the static parsing phase, it is the earliest execution in a module. The following code may not get the expected results.

  1. require('core-js/modules/es6.symbol');
  2. require('core-js/modules/es6.promise');
  3. import React from 'React';

5. The overall load of the module

In addition to specifying that an output value is loaded, you can also use the overall 星号 i.e. specify an object with an asterisk , and all output values are loaded on top of that object.

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

  1. // circle.js
  2. export function area(radius) {
  3. return Math.PI * radius * radius;
  4. }
  5. export function circumference(radius) {
  6. return 2 * Math.PI * radius;
  7. }

Now, load the module.

  1. // main.js
  2. import { area, circumference } from './circle';
  3. console.log('圆面积:' + area(4));
  4. console.log('圆周长:' + circumference(14));

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

  1. import * as circle from './circle';
  2. console.log('圆面积:' + circle.area(4));
  3. console.log('圆周长:' + circle.circumference(14));

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

  1. import * as circle from './circle';
  2. // 下面两行都是不允许的
  3. circle.foo = 'hello';
  4. circle.area = function () {};

6. Export default command

As you can see from the previous example, when using the import command, the user needs to know the variable name or function name to load, otherwise it cannot be loaded. However, users certainly want to get started quickly and may not want to read the documentation to understand what the properties and methods of the module are.

In order to make it easier for users to load modules without reading the documentation, the export default command is used to specify the default output for the module.

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

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

When another module loads the module, the import command can give the anonymous function any name.

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

The import command of the above code can point to the method of export-default .js output by any name, so you do not need to know the function name of the original module output. It is important to note that braces are not used after the import command.

The export default command is also useful before non-anonymous functions.

  1. // export-default.js
  2. export default function foo() {
  3. console.log('foo');
  4. }
  5. // 或者写成
  6. function foo() {
  7. console.log('foo');
  8. }
  9. export default foo;

In the code above, the function name foo of the foo function is not valid outside the module. When loaded, it is treated as an anonymous function load.

Here's a comparison of the default output with the normal output.

  1. // 第一组
  2. export default function crc32() { // 输出
  3. // ...
  4. }
  5. import crc32 from 'crc32'; // 输入
  6. // 第二组
  7. export function crc32() { // 输出
  8. // ...
  9. };
  10. import {crc32} from 'crc32'; // 输入

The two sets of writing in the above code, the first group is when using export default, the corresponding import statement does not need to use braces, and the second group does not use export default, the corresponding import statement needs to use braces.

The export default command specifies the default output of the module. O bviously, a module can have only one default output, so the export default command can only be used once. Therefore, the import command does not have to be parenthesed, because it is only possible to correspond only to the export default command.

Essentially, export default is the output of a variable or method called default, and then the system allows you to give it any name. Therefore, the following writing is valid.

  1. // modules.js
  2. function add(x, y) {
  3. return x * y;
  4. }
  5. export {add as default};
  6. // 等同于
  7. // export default add;
  8. // app.js
  9. import { default as foo } from 'modules';
  10. // 等同于
  11. // import foo from 'modules';

It is precisely because the export default command actually outputs a variable called default that it cannot be followed by a declaration statement with the variable.

  1. // 正确
  2. export var a = 1;
  3. // 正确
  4. var a = 1;
  5. export default a;
  6. // 错误
  7. export default var a = 1;

In the above code, export default a means assigning the value of variable a to the variable default. Therefore, the last way to write will be wrong.

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

  1. // 正确
  2. export default 42;
  3. // 报错
  4. export 42;

In the code above, the verse is wrong because the external interface is not specified, while the previous sentence specifies the external interface as default.

With the export default command, it's intuitive to enter a module, for example, a lodash module.

  1. import _ from 'lodash';

If you want to enter both the default method and other interfaces in an import statement, you can write this as below.

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

The export statement for the code above is as follows.

  1. export default function (obj) {
  2. // ···
  3. }
  4. export function each(obj, iterator, context) {
  5. // ···
  6. }
  7. export { each as forEach };

The last line of the code above means that the forEach interface is exposed, pointing by default to the each interface, i.e. forEach and each pointing to the same method.

Export default can also be used to output classes.

  1. // MyClass.js
  2. export default class { ... }
  3. // main.js
  4. import MyClass from 'MyClass';
  5. let o = new MyClass();

7. The compounding of export and import

If you enter and then output the same module in a module, import statement can export written together with the export statement.

  1. export { foo, bar } from 'my_module';
  2. // 可以简单理解为
  3. import { foo, bar } from 'my_module';
  4. export { foo, bar };

In the above code, the export and import statements can be combined and written in a single line. It is important to note, however, that after writing a line, foo and bar are not actually imported into the current module, but are equivalent to forwarding the two interfaces externally, resulting in the current module not being able to use foo and bar directly.

The module's interface is renamed and the overall output can also be written in this way.

  1. // 接口改名
  2. export { foo as myFoo } from 'my_module';
  3. // 整体输出
  4. export * from 'my_module';

The default interface is written as follows.

  1. export { default } from 'foo';

The named interface is written as follows.

  1. export { es6 as default } from './someModule';
  2. // 等同于
  3. import { es6 } from './someModule';
  4. export default es6;

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

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

Prior to ES2020, there was an import statement that did not have a corresponding compounding method.

  1. import * as someIdentifier from "someModule";

ES2020 complements this writing.

  1. export * as ns from "mod";
  2. // 等同于
  3. import * as ns from "mod";
  4. export {ns};

8. Inheritance of modules

模块 inherit 继承

Suppose you have a circleplus module that inherits the circle module.

  1. // circleplus.js
  2. export * from 'circle';
  3. export var e = 2.71828182846;
  4. export default function(x) {
  5. return Math.exp(x);
  6. }

The export in the code above represents all the properties and methods of the circle module that are then output. N ote that the export command ignores the default method of the circle module. The code then outputs the custom e variable and the default method.

At this point, you can also change the name of the circle's properties or methods and then output them.

  1. // circleplus.js
  2. export { area as circleArea } from 'circle';

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

Loading the module above is written as follows.

  1. // main.js
  2. import * as math from 'circleplus';
  3. import exp from 'circleplus';
  4. console.log(exp(math.e));

Import exp in the code above indicates that the default method for the circleplus module is loaded as the exp method.

9. Cross-module constants

As this book describes the const command, the constant declared by const is valid only for the current block of code. If you want to set a constant across modules (that is, across multiple files), or if a value is to be shared by multiple modules, you can write below.

  1. // constants.js 模块
  2. export const A = 1;
  3. export const B = 3;
  4. export const C = 4;
  5. // test1.js 模块
  6. import * as constants from './constants';
  7. console.log(constants.A); // 1
  8. console.log(constants.B); // 3
  9. // test2.js 模块
  10. import {A, B} from './constants';
  11. console.log(A); // 1
  12. console.log(B); // 3

If you want to use a very large number of constants, you can build a dedicated constants directory that writes various constants in different files and saves them in that directory.

  1. // constants/db.js
  2. export const db = {
  3. url: 'http://my.couchdbserver.local:5984',
  4. admin_username: 'admin',
  5. admin_password: 'admin password'
  6. };
  7. // constants/user.js
  8. export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

The constants of these file outputs are then merged .js index.

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

When you use it, load the index .js directly.

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

10. import()

Brief introduction

As mentioned earlier, import command is JavaScript by the JavaScript engine and executed before other statements within the module (the import command is actually more appropriate for "connecting" binding). Therefore, the following code will report an error.

  1. // 报错
  2. if (x === 2) {
  3. import MyModual from './myModual';
  4. }

In the above code, the engine processes the import statement at compile time, when the if statement is not analyzed or executed, so it makes no sense for the import statement to be placed in the if block of code, so a synth error is reported instead of an execution error. That is, the import and export commands can only be at the top of the module, not in blocks of code (for example, in if blocks of code, or in functions).

This design, while beneficial to the compiler, also prevents modules from being loaded at runtime. I n syntax, conditional loading is not possible. T his creates an obstacle if the import command is to replace Node's require method. Because require is a runtime load module, the import command cannot replace the dynamic loading feature of require.

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

The above statement is dynamic loading, need exactly which module to load, only the runtime to know. The import command does not do this.

The ES2020 proposal introduces an import() function that supports dynamic loading of modules.

  1. import(specifier)

In the above code, the parameter specifier of the import function specifies the location of the module to be loaded. The import command can accept what parameters, the import() function can accept what parameters, the difference between the two is mainly the latter for dynamic loading.

Import() returns a Promise object. Here's an example.

  1. const main = document.querySelector('main');
  2. import( ./section-modules/${someVariable}.js )
  3. .then(module => {
  4. module.loadPageInto(main);
  5. })
  6. .catch(err => {
  7. main.textContent = err.message;
  8. });

Import() functions can be used anywhere, not just modules, but also for non-module scripts. I t is runtime execution, that is, when this sentence is run, the specified module is loaded. I n addition, the import() function does not have a static connection relationship with the loaded module, which is not the same as the import statement. Import() is similar to Node's require method, with the main difference being that the former is an asynchronous load and the latter is a synchronous load.

Where applicable

Here are some of the applications for import().

(1) Load on demand.

Import() you can load a module when you need it.

  1. button.addEventListener('click', event => {
  2. import('./dialogBox.js')
  3. .then(dialogBox => {
  4. dialogBox.open();
  5. })
  6. .catch(error => {
  7. /* Error handling */
  8. })
  9. });

In the code above, the import() method is placed in the listening function of the click event, and the module is loaded only if the user clicks the button.

(2) Conditional loading

Import() can be placed in an if block of code and load different modules depending on the situation.

  1. if (condition) {
  2. import('moduleA').then(...);
  3. } else {
  4. import('moduleB').then(...);
  5. }

In the above code, if the conditions are met, module A is loaded, otherwise module B is loaded.

(3) Dynamic module path

Import() allows the module path to be generated dynamically.

  1. import(f())
  2. .then(...);

In the code above, different modules are loaded based on the return result of function f.

Note the point

After import() loads the module successfully, it is treated as an object as an argument to the then method. Therefore, you can use the object deconstructing the syntax of the assignment to get the output interface.

  1. import('./myModule.js')
  2. .then(({export1, export2}) => {
  3. // ...·
  4. });

In the code above, export1 and export2 are both output interfaces .js myModule, which can be deconstructed.

If the module has a default output interface, it can be obtained directly with parameters.

  1. import('./myModule.js')
  2. .then(myModule => {
  3. console.log(myModule.default);
  4. });

The above code can also be entered in the form of a name.

  1. import('./myModule.js')
  2. .then(({default: theDefault}) => {
  3. console.log(theDefault);
  4. });

If you want to load multiple modules at the same time, you can do so using the following writing.

  1. Promise.all([
  2. import('./module1.js'),
  3. import('./module2.js'),
  4. import('./module3.js'),
  5. ])
  6. .then(([module1, module2, module3]) => {
  7. ···
  8. });

Import() can also be used in async functions.

  1. async function main() {
  2. const myModule = await import('./myModule.js');
  3. const {export1, export2} = await import('./myModule.js');
  4. const [module1, module2, module3] =
  5. await Promise.all([
  6. import('./module1.js'),
  7. import('./module2.js'),
  8. import('./module3.js'),
  9. ]);
  10. }
  11. main();