Introduction to JavaScript modules

JavaScript modules allow us to divide code into small pieces. They also let us keep some code private while exposing other pieces of code that can be imported into another module.

In this article, we’ll look at how to define and use modules.

Named exports

Named exports start with the export keyword – they expose items from inside a module to the outside. Then, we can import them somewhere else.

There can be multiple named exports in one module. For instance, we can write:

export const foo = 1;  
export let bar = 1;

Then, we can import them into another module as follows:

import { foo, bar } from "./module";
const baz = foo + bar;

We can also import the whole module with the * sign as follows:

import * as module from "./module";
const baz = module.foo + module.bar;
index.js
module.js
import { foo, bar } from "./module";
const baz = foo + bar;
// priting
console.log(baz)

Try out the second method.

Default export

We can export one default export using the export default keywords. For instance, we can write:

export default 1;

Then, import it as follows:

import foo from "./bar";  
const baz = foo;

We can also export functions and class as follows:

export default () => {};

Or:

export default class {}

We don’t need a semicolon at the end of the class export. Also, we can define default exports with the following code:

const foo = 1;  
export { foo as default };

Browser differences between scripts and modules

In browsers, scripts are denoted by the script tag. Modules are the same, but it’s denoted by the type attribute with the value module.

  • Scripts are not in strict mode by default, but modules are.

  • Top-level variables are global in scripts, but they are local to the module in modules.

  • The top-level value of this is window in scripts and undefined in modules.

  • Scripts are run synchronously, while modules are run asynchronously.

  • There are no import statements in scripts, but we can selectively import module members in modules.

We can programmatically import both scripts and modules using promise-based APIs.

Module characteristics

An ES6 module can be statically analyzed for static checking, optimization, and more. It has a declarative syntax for importing and exporting.

Imports are hoisted to the top so that they can be referenced anywhere in the module.

For instance, if we have:

export const foo = 1;

Then, we can import it as follows:

import { foo } from "./bar";
const baz = foo;

Also, they must be at the top-level. Therefore, we can’t have something like this:

const baz = foo; 
import { foo } from "./bar"; 

Imports are read-only views on exports

ES6 imports are read-only views on export entities. Connections to variables inside the module that imported the export remain live.

For instance, if we have:

export const foo = 1;

Then, if we have the following:

import { foo } from "./bar";  
foo = 1;

Then, we’ll get a read-only error. Let’s see this in the code snippet below:

index.js
module.js
import { foo } from "./bar";
foo = 1;

We get the same result if we change the const to let.

Cyclic dependencies

If module A and B import members from each other, then we call it a cyclic dependency. This is supported with ES6 modules. For instance, if we have:

index.js:

import { foo } from "./bar";
export const baz = 2;

bar.js:

import { baz } from "./index";
export let foo = 1;

Then, the modules are cyclic dependencies since we can import a member from bar in index, and import a member from index in bar.

This works because imports just refer to the original data, so it doesn’t matter when they come from.

Importing styles

We can import JavaScript modules in various ways. One way is the default import, which is how we import members from a module.

For instance, we can write:

bar.js:

let foo = 1;  
export default foo;

Then, we can import it as follows:

import foo from "./bar";

Named imports can be imported as follows (given the following named exports):

export let foo = 1;

Then, we can import it as follows:

import { foo } from "./bar";

We can rename named exports by using the as keyword as follows:

import { foo as baz } from "./bar";

We can also rename default exports as follows:

import { default as foo } from "./bar";

We can also have empty where we don’t import anything. Instead, we run what’s included in the module.

For instance, if we have the following in bar.js:

console.log("bar");

Then, we can run the code in bar.js as follows:

import "./bar";

Therefore, we should see 'bar' logged in the console log.

Conclusion

ES6 modules are a great way to divide code into small chunks. We can export module members and import them in another file. Imported members are read-only. Modules are in strict mode by default, which allows us to avoid a lot​ of the issues that come with non-strict mode.

Attributions:
  1. undefined by undefined