JavaScript Modules

by Alexandra October. 26, 17 0 Comment
JavaScript

This blog post is a part of our ES6 series tutorials where we will make an overview of the new ES6 module syntax.

Intro in JS Modules

Modular programming is a key concept in programming. It allows us to divide our work into multiple files and make them access one another. This is a common design pattern in every object-oriented programming language. First built-in support for modules in JavaScript appears with the 6th edition of the ECMAScript standard (before that the community had to use libraries that allow writing modular code).

Essentially, modules in ES6 are files that contain JavaScript code, that automatically runs in strict mode (and there is no way to opt-out). That code may contain declarations as variable declarations, function declarations, class declarations, etc. These declarations by default remain local to the module unless you explicitly export them in order to make them accessible by other modules. ES6 modules syntax is very declarative: to export certain declarations you use the keyword export, to consume the exported declarations in a different module you use import.

Note: Before we move on, we have to mention that this feature is just beginning to be implemented in browsers natively. It is only supported by Chrome 61+ and Safari 10.1+. Nevertheless, if you want to make modules work on the web do the following:

  •  In Chrome, you can run them through a local web server because their MIME types have to be validated. This can be hard for new users and is slower than double clicking your file.
  • Run them in Safari, you either have to download a version from 2012 or you have to be a Mac user.

Other browsers only implement modules experimentally, which is buggy and not perfect for serious development.

This is where GT2 comes in, it supports modules and also serves the purpose of a web server, so you execute every file as you would normally do.

Now, let’s take a look at a simple native module example:

Here is the module file:

And the imported utils:

The following picture shows the result in the browser, ‘Hello! I am imported.’ is logged:

module

Let’s check out the scope of a module’s variable. As we said above, it is local to the module it was declared in unless you export it:

The module:

The exported variable:

The value of the variable is logged:

variable

But the following will produce an error about attempting to reassign a read-only property. Every import is a live connection to the exported data. Imports are read-only.
Unqualified imports (import x from 'foo') are like const-declared variables:

error

Let’s check that the native modules are in strict mode:

Another example that illustrates the strict mode with inline scripts in HTML:

Which will produce different results:

type module

In addition to the compact syntax for importing and exporting, ES6 modules have substantial scoping benefits and support for cyclic dependencies between modules. We will look at each of these advantages below.

Modules’ syntax and semantics

There are two kinds of exports:

  •  a named export: useful when exporting multiple declarations. You can actually rename imports and exports which solves the name collision problem.
  •  a default export: one per module, ES6 favors the single/default export style and gives the sweetest syntax to importing the default.

Let’s look at the different ways of exporting and importing in ES6:

Exporting

As we have seen, you can use the export keyword to export parts of the code to make it public to other modules.

  •  Named export

Note: Unless you are using default keyword, every class or function declarations require a name.

Importing

To consume exported declarations in another module we use the import keyword. The first part of import statement is the bindings you аre importing, the second part is the path to the module from which you are importing. The path can be relative to the location of the importing module or absolute, pointing directly to the file of the module to be imported.

Ways to import bindings:

  •  single binding

 

  • multiple bindings

  •  import everything

 

Renaming Exports and Imports

Notice that in the last example, all exported bindings are loaded into an object example. Sometimes you may want to use a different name for an imported variable. You can use the as keyword to do that:

 

You could name the function something else to avoid name collisions:

  •  Default export

The default value for a module is a single variable, function or class, you can only set one default export per module.

Importing default values:

Note: No curly braces are used.

There are modules that export both a default and non-default bindings:

 

Note: The comma that separates the bindings, also the default comes before the non-defaults.

You can use the renaming syntax too:

Scoping benefits

One of the main goals ES6 has is to solve the scope problem that JavaScript owns (that everything shares one global scope). That is where the modules come in. Declarations created in the top level of a module are not automatically added to the shared global scope. They exist only within the top-level scope of the module. However, the value of this in the module is undefined.
In order to load modules with different restrictions preset, modules have to be explicitly declared as such with the type="module" syntax. More information about module loading, you can find here.

Cyclic dependencies

First of all, let’s make it clear what cyclical dependencies are. They appear when you have two files which import one another (e.g. A.js imports B.js and B.js imports A.js). It is not a recommended design pattern because it leads to that both files can be used and evolved only together. Nevertheless, ES6 supports cyclic dependencies, because there are some edge cases where you need this kind of behavior. For example, tree structures can use cyclic dependencies when child nodes refer to their parents (e.g. the DOM).

Let’s compare how CommonJS (library that allows modular programming) and ES6 handle cyclic dependencies. In CommonJS, if module B requires module A whose body is currently being evaluated, it gets back A‘s exports object in its current state (line 1). That enables B to refer to properties of that object inside its exports (line 2). The properties are filled in after B’s evaluation is finished, at which point B’s exports work properly.

But CommonJS has two limitations:

  •  in Node.js single-value exports don’t work.

If you do that in module A:

module.exports = function () { ... }

you would not be able to use the exported function in module B because B‘s variable a would still refer to A‘s original exports object.

  •  you can’t use named exports directly.

That is, module B can’t import a.foo like this:

foo = require('a').foo

foo would simply be undefined.

ES6 way to handle the listed limitations is that modules export bindings, not values, this guarantees that the connection to variables declared inside the module body stays alive:

It does not matter whether you access a named export directly or via its module, there is an indirection involved in either case and it always works.

Further reading

JavaScript modules for C# developers

ECMAScript 6 modules: the final syntax

export – JavaScript | MDN

import – JavaScript | MDN

 

We hope that this initial blog post gave you a detailed understanding of ES6 module syntax. If you have any questions write a comment below, tweet us or start a discussion in our Forum.

Social Shares

Related Articles

Leave a Comment