Subtleties of the modular system ECMAScript 2015 (ES6)

    For about six months now I have been writing on ES6 (which was eventually called ES-2015) and ES7, using the babel as a translator. I had to write mainly the server side, respectively, the use of modules was taken for granted: before ES6, using the module system of the node itself , and now, using the standardized semantics of the language itself. And I wanted to write an article in which to paint the subtleties, pluses, pitfalls and peculiarities of the newfound modular system of the language: partly - so that it would be easier for others, partly - to understand everything completely myself.

    I’ll analyze 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


    Essentially, the module - is an instruction ( statement ), which is caused indirectly - by creating a file and execute it with the interpreter ES (straight, at "start-up" file a programmer, or indirectly, by importing another module). There is a clear correlation in ES6: one file - one module. Each module has a separate scope ( Lexical environment ) - that is, all declarations of variables, functions and classes will not be accessible outside the module (file) unless they are exported explicitly. 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 statement


    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 links to all exported entities, and the key is an entity identifier (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; we will consider all possible cases.

    Export declared entity


    Essentially, it is the usual declaration of a variable, function, or class, with the keyword " export " 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 declare the entity first 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. The object property NodeJS exports is set to expression. In ES6, the export statement adds a link (or binding ) to the declared entity in [[ Exports ]] . This means that [[ Exports ]]. < Entity name > will always return the current value of this entity.

    export var bla = 10; // [[Exports]].bla === 10
    bla = 45; // [[Exports]].bla === 45
    

    Export an already declared entity


    Everything is the same here, we only export the entity that was already announced above. For this, curly brackets are used after the export keyword , in which all entities (well, their identifiers - for example, the name of the variable) that need to be exported are indicated with a comma.

    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 keeps an entity reference, even in the case of “renaming”.

    var bar = 10;
    export { bar as bla }; // [[Exports]].bla === 10
    bar = 42; // [[Exports]].bla === 42
    

    Default Export


    This use case for export differs from the two described above, and, in my opinion, it is a little illogical. It consists in using the default keyword after export , after which one of the three can follow: expression, function declaration, class declaration.

    export default 42; 
    

    export default function func() {}
    

    export default class Foo {}
    

    Each of these three use cases adds a property with the default key to [[ Exports ]]. Exporting the default expression (the first example, “export default 42;”) is the only case when using export , when the value of the [[ Exports ]] property becomes not the reference to any entity, but the value of the expression. In the case of exporting a function by default (not anonymous, of course) or a 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 narrative, I will continue right away about the default import.

    The default exported property is considered the "main" in this module. Its import is carried out using the import statement of the following modification:

    import <любое имя> from '<путь к модулю>';
    

    This is the whole benefit of default export - you can call it whatever you like with import.

    // sub.js
    export default class Sub {};
    // main.js
    import Base from './sub.js'; // И да, иногда это может сбить столку, поэтому лучше всё же использовать имя модуля
    

    Importing regular exported properties looks a bit different:

    // file1.js
    export let bla = 20;
    // file2.js
    import { bla } from './file1.js'; // нужно точно указать имя сущности
    // file3.js
    import { bla as scopedVariable } from './file1.js'; // но можно переименовать
    

    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 the binding of the identifier “ bla ” to the value [[ Exports ]]. Bla. That is, just like [[ 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 module "file3.js".

    // count.js
    export let counter = 0;
    export function inc() {
      ++counter;
    }
    // main.js
    import { counter, inc } from './count.js'; 
    console.log(counter); // 0
    inc();
    console.log(counter); // 1
    

    Import all exported properties


    import * as sub from './sub.js';
    

    In essence, this way we get a copy of the [[ Exports ]] module “sub.js”.

    Enabling a module without import


    Sometimes it is necessary that the file simply starts.

    import './worker';
    

    Re-export


    The last thing that I will discuss here is re-exporting by a module a property that it imports from another module. This is done by the export statement .

    // main.js
    export { something } from './another.js';
    


    Two notes that are worth making here: the first - something after re-export does NOT become available inside the main.js module, you will have to make a separate import for this (I don’t know why, apparently, to preserve the spirit of the export operator ); and second, the link system works here: a module that imports something from “main.js” will receive the actual value of the something variable in “another.js”;

    You can also export all properties from another module.

    export * from './another';
    

    However, it is important to remember that if you declare an export with the same name as the re-export in your module, it will overwrite the re-export.

    // sub.js
    export const bla = 3, foo = 4;
    // another.js
    export const bla = 5;
    export * from './sub';
    // main.js
    import * as another from './another';
    console.log(another); // { bla: 5, foo: 4 }
    

    This is solved by renaming conflicting properties during re-export.

    And, for some reason, export does not have syntax for re-exporting default properties , but you can do this:

    export { default as sub } from './sub';
    

    A few words about import properties


    Loopback support


    Actually, all this dance with binders, instead of assignment, is needed for the normal resolution of circular references. Since this 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.

    Imports pop up


    Imports pop up at the top of the module.
    // sub.js
    console.log('sub');
    // main.js
    console.log('main');
    import './sub.js';
    

    If you run main.js, then “sub” will be displayed in the console first, and only then “main” - precisely because of the emergence of imports.

    The default export is not the end


    These designs are quite acceptable.

    // jquery.js
    export default function $() {}
    export const AUTHOR = "Джон Резиг";
    // main.js
    import $, { AUTHOR } from 'jquery.js';
    

    In general, in fact, default is just another named export.

    import Base from './Base';
    

    Same 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).

    Also popular now: