For about six months I have been writing on ES6 (which was eventually called ES-2015) and ES7, using
Babel as a translator. I had to write mainly the server part, respectively, the use of modules was taken for granted: before ES6, with the help of the
modular system of the node itself , and now with the help of the
standardized semantics of the language itself. And I wanted to write an article in which to paint the subtleties, advantages, pitfalls and unusual features of the new-found modular language system: partly - so that it would be easier for others, partly - to understand everything completely.
I will understand what a module is, how entities are exported, how entities are imported, how the ES6 module system differs from the module system in NodeJS.
Module
In essence, a module is an instruction (
statement ) that is invoked implicitly - by creating a file and executing it using the ES interpreter (directly, when the file is “started” by the programmer, or indirectly, as a result of import by another module). In ES6, there is a clear correlation: one file - one module. Each module has a separate
Lexical environment — that is, all declarations of variables, functions, and classes will not be accessible outside the module (file) unless explicitly exported. At the top level of a module (that is, outside of other instructions and expressions), you can use
import statements to import other modules and their exported entities, and
export to export your own module entities.
Export operator
The
export statement allows you to export module entities so that they are accessible from other modules. Each module has an implicit [[
Exports ]] object, which stores references to all exported entities, and the key is the identifier of the entity (for example, the name of a variable). This is very similar to
module.exports from the
NodeJS modular system, but [[
Exports ]] is
always an object and cannot be obtained directly. The only way to change it is to use the
export statement.
')
This operator has several modifications, consider all possible cases.
Export Declared Entity
In essence, this is a regular declaration of a variable, function, or class, with the "
export " keyword in front of it.
export var variable; export const CONSTANT = 0; export let scopedVariable = 20; export function func(){}; export class Foo {};
In this case, the ES6 export system is more convenient than in NodeJS, where you would have to first declare the entity, and then add it to the
module.exports object.
var variable; exports.variable = variable; const CONSTANT = 0; exports.CONSTANT = CONSTANT; ...
But there is a much more important difference between the two systems. In the NodeJS property of the
exports object, the value of the expression is assigned. In ES6, the
export statement adds to [[
Exports ]] a link (or
binding ) to the declared entity. This means that [[
Exports ]]. <
Entity name > will always return the current value of this entity.
export var bla = 10;
Export of already declared entity
Here everything is the same, only we export the entity that has already been declared above. To do this, use curly brackets after the
export keyword, in which all entities (well, their identifiers — for example, the name of a variable) are to be exported, separated by commas.
var bar = 10; function foo() {} export { bar, foo };
Using the "
as " keyword, you can "rename" an entity during export (more precisely, change the key for [[
Exports ]]).
var bar = 10; function foo() {} export { bar as bla, foo as choo };
For this type of export, it is also true that [[
Exports ]] only stores an entity reference, even in the case of “renaming”.
var bar = 10; export { bar as bla };
Export by default
This option of using
export is different from the two described above, and, in my opinion, it is a little illogical. It consists in using the
export keyword after the
default , after which one of three
things can go: an expression, a function declaration, a class declaration.
export default 42;
export default function func() {}
export default class Foo {}
Each of these three use cases adds a property with the key “default” to the [[
Exports ]]. Exporting a default
expression (the first example, “export default 42;”) is the only case when using
export , when the value of the [[
Exports ]] property becomes not a reference to any entity, but the value of an expression. In the case of the default export function (not anonymous, of course) or class - they will be declared in the scope of the module, and [[
Exports ]]. Default will be a reference to this entity.
Import statement
In order not to break the story, I will continue immediately on import by default.
The exported property is considered the “main” property in this module. Its import is carried out using the
import statement of the following modification:
import < > from '< >';
This is all the benefits of default export - when importing, you can call it somehow.
Importing normal exported properties looks a bit different:
Consider the module "file2.js". The
import statement receives the [[
Exports ]] object of the imported module ('file1.js'), finds the desired property (“bla”) in it, and then creates a binding identifier “
bla ” to the value of [[
Exports ]]. Bla. That is, in the same way as [[
Exports ]]. Bla,
bla in the module file2.js will always return the current value of the variable bla from the module file1.js. As well as
scopedVariable from the «file3.js» module.
Import all exported properties
import * as sub from './sub.js';
In essence, this is how we get a copy of the [[
Exports ]] module of the "sub.js" module.
Enable module without import
Sometimes it is necessary to just start the file.
import './worker';
Re-export
The last thing I’ll consider here is that the module re-exports the property that it imports from another module. This is done by the
export operator.
Two remarks that should be made here: first,
something after re-export does NOT become available inside the main.js module, for this you have to do a separate import (I don’t know why, apparently, to keep the spirit of the
export operator); and second, the link system works here too: a module that imports from “main.js”
something will receive the current value of the variable something in “another.js”;
You can also re-export
all properties from another module.
export * from './another';
However, it is important to remember that if you declare in your module an export with the same name as that of the re-export, it will erase the re-export.
This is solved by renaming the conflicting properties upon re-export.
And, for some reason, there is no syntax for re-export of default properties, but you can do this:
export { default as sub } from './sub';
A few words about the properties of imports
Circular link support
Actually, this whole dance with binders instead of assignment is needed for the normal resolution of circular references. Because it is not a value (which may be undefined), but a link to the place where something will once lie - nothing will fall, even if the cycle is.
Imports float
Imports "float" up the module.
If you run main.js, then the first “sub” is displayed in the console, and only then the “main” is due to the emergence of imports.
The default export is not the end.
These constructions are quite acceptable.
And in general, in fact, default is just another named export.
import Base from './Base';
Same thing as
import { default as Base } from './Base';
Thank you very much for reading the article, I hope it will save you time and generally help. I will be glad to hear questions and help).