Module syntax

Overview

Historically, JavaScript has never had a module system, and it is impossible to split a large program into small files that depend on each other, and then assemble them in simple ways. Other languages ​​have this feature, such as Ruby's require, Python's import, and even CSS has @import, but JavaScript does not have any support for this, which is great for the development of large and complex The project formed a huge obstacle.

Before ES6, the community developed some module loading schemes, the most important of which are CommonJS and AMD. The former is used for servers and the latter is used for browsers. ES6 implements module functions at the level of language standards, and the implementation is quite simple. It can completely replace CommonJS and AMD specifications and become a common module solution for browsers and servers.

The design idea of ​​ES6 modules is to be as static as possible, so that the dependencies of the modules, as well as the input and output variables, can be determined at compile time. Both CommonJS and AMD modules can only determine these things at runtime. For example, CommonJS modules are objects, and object attributes must be searched when typing.

// 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 to load the fs module as a whole (that is, load all the methods of fs), generate an object (_fs), and then read 3 methods from this object. This kind of loading is called "runtime loading", because this object can only be obtained at runtime, making it impossible to do "static optimization" at compile time.

ES6 modules are not objects, but the output code is explicitly specified through the export command, and then input through the 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, other methods are not loaded. This kind of loading is called "compile-time loading" or static loading, that is, ES6 can complete module loading at compile time, and the efficiency is higher than that of CommonJS modules. Of course, this also makes it impossible to reference the ES6 module itself, because it is not an object.

Because ES6 modules are loaded at compile time, static analysis is possible. With it, the syntax of JavaScript can be further broadened, such as the introduction of macros and type systems, which can only be achieved by static analysis.

In addition to the various benefits of static loading, ES6 modules have the following benefits.

-The UMD module format is no longer needed. In the future, both servers and browsers will support the ES6 module format. At present, through various tool libraries, this has actually been achieved. -In the future, the new API of the browser will be provided in module format, and it will no longer be necessary to make global variables or attributes of the navigator object. -Objects are no longer needed as namespaces (such as Math objects), and these functions can be provided through modules in the future.

This chapter introduces the syntax of ES6 modules, and the next chapter introduces how to load ES6 modules in the browser and Node.

Strict mode

ES6 modules automatically adopt strict mode, regardless of whether you add "use strict"; to the head of the module.

Strict mode mainly has the following restrictions.

-Variables must be declared before use -Function parameters cannot have attributes with the same name, otherwise an error will be reported -Cannot use with statement -Cannot assign values ​​to read-only attributes, otherwise an error will be reported -The prefix 0 cannot be used to represent an octal number, otherwise an error will be reported -Cannot delete non-deletable attributes, otherwise an error will be reported -The variable delete prop cannot be deleted, an error will be reported, only the property delete global[prop] can be deleted -eval does not introduce variables in its outer scope -eval and arguments cannot be reassigned -arguments will not automatically reflect changes in function parameters -Cannot use arguments.callee -Cannot use arguments.caller -Disallow this to point to the global object -Cannot use fn.caller and fn.arguments to get the stack of function calls -Added reserved words (such as protected, static and interface)

All the above restrictions must be observed by the module. Since strict mode was introduced by ES5, it does not belong to ES6, so please refer to related ES5 books, this book will not introduce it in detail.

Among them, special attention should be paid to the limitation of this. In ES6 modules, the top-level this points to undefined, that is, this 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 import functions provided by other modules.

A module is an independent file. All the variables inside the file cannot be obtained from the outside. If you want the external to be able to read a variable inside the module, you must use the export keyword to export the variable. Below is a JS 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 the profile.js file, which saves user information. ES6 regards it as a module, which uses the export command to export three variables to the outside.

Besides the above, there is another way to write export.

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

export { firstName, lastName, year };

The above code is after the export command, using curly braces to specify a set of variables to be exported. It is equivalent to the previous way of writing (placed directly before the var statement), but this way of writing should be given priority. Because in this way, you can see which variables are output at a glance at the end of the script.

In addition to outputting 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 externally.

Normally, the variables exported by export have their original names, but they 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 interfaces of the functions v1 and v2. After renaming, v2 can be output twice with different names.

It is important to note that the export command specifies the external interface, which must establish a one-to-one correspondence with the internal variables of the module.

// report an error
export 1;

// report an error
var m = 1;
export m;

The above two writing methods will report an error because there is no external interface provided. The first way of writing directly outputs 1 and the second way of writing uses the variable m to output 1 directly. 1 is just a value, not an interface. The correct writing is as follows.

// Writing method one
export var m = 1;

// Writing method two
var m = 1;
export { m };

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

The above three wordings are correct and specify the external interface m. Other scripts can get the value 1 through this interface. Their essence is to establish a one-to-one correspondence between the interface name and the internal variables of the module.

Similarly, the output of function and class must also follow this way of writing.

// report an 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 its corresponding value, that is, through this interface, the real-time value inside the module can be obtained.

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

The above code outputs the variable foo, the value is bar, and it becomes baz after 500 milliseconds.

This is completely different from the CommonJS specification. The output of the CommonJS module is the value cache, and there is no dynamic update. For details, see the section "Module Loading Implementation" below.

Finally, the export command can appear anywhere in the 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 will the import command in the next section. This is because in the conditional code block, static optimization cannot be done, 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 reported.

import command

After using the export command to define the external interface of the module, other JS files can load the module through the import command.

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

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

The import command in the above code is used to load the profile.js file and input variables from it. The import command accepts a pair of curly braces, which specify the names of variables to be imported from other modules. The variable name in the braces must be the same as the name of the external interface of the imported module (profile.js).

If you want to rename the input variable, the import command should use the as keyword to rename the input variable.

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

The variables input by the import command are all 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 the variable a, and re-assigning it will report an error because a is a read-only interface. However, if a is an object, rewriting the attributes of a is allowed.

import { a } from "./xxx.js";

a.foo = "hello"; // legal operation

In the above code, the attribute of a can be successfully rewritten, and other modules can also read the rewritten value. However, this writing method is difficult to check errors. It is recommended that all input variables be treated as completely read-only, and do not easily change its attributes.

The from after import specifies the location of the module file, which can be a relative path or an absolute path. If there is no path, just a module name, then there must be a configuration file that tells the JavaScript engine the location of the module.

import { myMethod } from "util";

In the above code, util is the module file name. Since it does not have a path, it must be configured to tell the engine how to get this module.

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

foo();

import { foo } from "my_module";

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

Since import is executed statically, expressions and variables cannot be used. These grammatical structures can only be obtained at runtime.

// report an error
import {'f' +'oo'} from'my_module';

// report an error
let module ='my_module';
import {foo} from module;

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

The above three writing methods will report errors because they use expressions, variables and if structures. In the static analysis stage, these grammars have no value.

Finally, the import statement will execute the loaded module, so the following can be written.

import "lodash";

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

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

import "lodash";
import "lodash";

The above code loads lodash twice, but only executes it once.

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

// Equivalent to
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 module. In other words, the import statement is in Singleton mode.

At this stage, through Babel transcoding, the require command of the CommonJS module and the import command of the ES6 module can be written in the same module, but it is best not to do so. Because import is executed in the static analysis phase, it is the earliest executed in a module. The following code may not get the expected result.

require("core-js/modules/es6.symbol");
require("core-js/modules/es6.promise");
import React from "React";

Overall loading of the module

In addition to specifying to load a certain output value, you can also use overall loading, that is, use an asterisk (*) to specify an object, and all output values ​​are loaded on this object.

Below is a circle.js file, which 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("Circle area:" + area(4));
console.log("Circumference length:" + circumference(14));

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

import * as circle from "./circle";

console.log("Circle area:" + circle.area(4));
console.log("Circumference length:" + circle.circumference(14));

Note that the object where the module is loaded as a whole (circle in the above example) should be statically analyzable, so it is not allowed to change at runtime. The following writing is not allowed.

import * as circle from "./circle";

// The following two lines are not allowed
circle.foo = "hello";
circle.area = function () {};

export default command

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

In order to provide users with convenience, so that they can load the module without reading the document, it is necessary to use the export default command to specify the default output for the module.

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

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

When other modules load the module, the import command can specify any name for the anonymous function.

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

The import command in the above code can use any name to point to the output method of export-default.js, and then there is no need to know the name of the function output by the original module. It should be noted that at this time, the braces are not used after the import command.

It is also possible to use the export default command before non-anonymous functions.

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

// Or written as

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

export default foo;

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

Let's compare the default output and normal output.

// First group
export default function crc32() {
  // output
  // ...
}

import crc32 from "crc32"; // input

// Second Group
export function crc32() {
  // output
  // ...
}

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

The two groups of writing of the above code, the first group is when export default is used, the corresponding import statement does not need to use braces; the second group is when the export default is not used, the corresponding import statement needs to be used big parantheses.

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 increase the brackets after the import command, because it can only correspond to the export default command.

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

// modules.js
function add(x, y) {
  return x * y;
}
export { add as default };
// Equivalent to
// export default add;

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

It is precisely because the export default command actually only outputs a variable called default, so it cannot be followed by 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, the meaning of export default a is to assign the value of the variable a to the variable default. Therefore, the last wording will report an error.

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

// correct
export default 42;

// report an error
export 42;

In the above code, the latter sentence reports an error because the external interface is not specified, while the former sentence specifies the external interface as default.

With the export default command, the input module is very intuitive, take the input lodash module as an example.

import _ from "lodash";

If you want to enter the default method and other interfaces in a single import statement, you can write it as follows.

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 };

The last line of the above code means that the forEach interface is exposed, which defaults to the each interface, that is, forEach and each point to the same method.

export default can also be used to export classes.

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

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

Compound writing of export and import

If in a module, the same module is imported first and then exported, the import statement can be written together with the export statement.

export { foo, bar } from "my_module";

// can be simply understood as
import { foo, bar } from "my_module";
export { foo, bar };

In the above code, the export and import statements can be combined and written in one line. However, it should be noted that after writing into one line, foo and bar are not actually imported into the current module, but are equivalent to forwarding these two interfaces to the outside, resulting in the current module not being able to directly use foo and bar .

The interface rename and overall output of the module can also be written in this way.

// Rename the interface
export { foo as myFoo } from "my_module";

// Overall output
export * from "my_module";

The default interface is written as follows.

export { default } from "foo";

The wording of changing the named interface to the default interface is as follows.

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

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

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

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

Before ES2020, there was an import statement, and there was no corresponding compound wording.

import * as someIdentifier from "someModule";

ES2020 added this wording.

export * as ns from "mod";

// Equivalent to
import * as ns from "mod";
export { ns };

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);
}

The export * in the above code means to export all the attributes and methods of the circle module. Note that the export * command will ignore the default method of the circle module. Then, the above code outputs the custom e variable and default method.

At this time, you can also rename the attributes or methods of circle before outputting them.

// circleplus.js

export { area as circleArea } from "circle";

The above code indicates that only the area method of the circle module is output, and it is renamed to circleArea.

The writing method of loading the above module is as follows.

// main.js

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

The import exp in the above code means that the default method of the circleplus module is loaded as the exp method.

Cross-module constants

When the book introduced the const command, it was said that the constants declared by const are only valid in the current code block. If you want to set a constant across modules (that is, across multiple files), or that a value needs to be shared by multiple modules, you can use the following writing method.

// 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 there are a lot of constants to be used, 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 by these files into index.js.

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

When using it, just load index.js directly.

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

import()

Introduction

As mentioned earlier, the import command will be statically analyzed by the JavaScript engine and executed before other statements in the module (the import command is called "connection" binding, which is more appropriate). Therefore, the following code will report an error.

// report an error
if (x === 2) {
  import MyModual from "./myModual";
}

In the above code, the engine processes the import statement at compile time. At this time, it will not analyze or execute the if statement, so the import statement is meaningless in the if code block, so it will report the syntax Error, not an execution error. In other words, the import and export commands can only be at the top level of the module, not in the code block (for example, in the if code block, or in the function).

Such a design is certainly conducive to the efficiency of the compiler, but it also makes it impossible to load modules at runtime. Syntactically, conditional loading is impossible. If the import command is to replace Node's require method, this becomes an obstacle. Because require loads modules at runtime, the import command cannot replace the dynamic loading function of require.

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

The above statement is dynamic loading. Which module is loaded by require is only known at runtime. The import command cannot do this.

ES2020 proposal Introduce the import() function to support dynamic loading of modules.

import(specifier);

In the above code, the parameter specifier of the import function specifies the location of the module to be loaded. What parameters can the import command accept and what parameters can the import() function accept. The main difference between the two is that the latter is dynamically loaded.

import() returns a Promise object. Below is an example.

const main = document.querySelector("main");

import(`./section-modules/${someVariable}.js`)
  .then((module) => {
    module.loadPageInto(main);
  })
  .catch((err) => {
    main.textContent = err.message;
  });

The import() function can be used anywhere, not only in modules, but also in non-module scripts. It is executed at runtime, that is, when it runs to this sentence, the specified module will be loaded. In addition, the import() function has no static connection relationship with the loaded module, which is also different from the import statement. import() is similar to Node's require method. The main difference is that the former is loaded asynchronously, and the latter is loaded synchronously.

Applications

The following are some applications of import().

(1) Load on demand.

import() can load a module when needed.

button.addEventListener("click", (event) => {
  import("./dialogBox.js")
    .then((dialogBox) => {
      dialogBox.open();
    })
    .catch((error) => {
      /* Error handling */
    });
});

In the above code, the import() method is placed in the listener function of the click event, and this module will be loaded only when the user clicks the button.

(2) Conditional loading

import() can be placed in the if code block to load different modules according to different situations.

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

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

(3) Dynamic module path

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

import(f())
.then(...);

In the above code, according to the return result of the function f, different modules are loaded.

be careful

After the import() successfully loads the module, the module will be used as an object as a parameter of the then method. Therefore, you can use the syntax of object deconstruction assignment to get the output interface.

import("./myModule.js").then(({ export1, export2 }) => {
  // ...·
});

In the above code, both export1 and export2 are the output interfaces of myModule.js, which can be obtained by deconstruction.

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

import("./myModule.js").then((myModule) => {
  console.log(myModule.default);
});

The above code can also use the form of named input.

import("./myModule.js").then(({ default: theDefault }) => {
  console.log(theDefault);
});

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

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import() can also be used in async functions.

async function main() {
  const myModule = await import("./myModule.js");
  const { export1, export2 } = await import("./myModule.js");
  const [module1, module2, module3] = await Promise.all([
    import("./module1.js"),
    import("./module2.js"),
    import("./module3.js"),
  ]);
}
main();