Skip to main content

JavaScript Modules Explained: Tutorial for Beginners

A brief history of JavaScript modules

Technically speaking, developers have been using modules in JavaScript for some time now. Solutions like CommonJS (for Node.js), Browserify, and Require.js have allowed developers to separate their code into reusable modules that make the code easier to maintain.

CommonJS was essentially the basis for Node’s package management system, npm. This allowed developers to create packages, or modules, that they can add to a remote registry, allowing others to use them. They, too, could use modules on npm’s registry for their own projects. But this was exclusive to Node.js (backend JavaScript), so browsers (i.e., client-side JavaScript) didn’t have a way to incorporate this modular package management.

That’s why JavaScript developers created tools like Browserify and Require.js to incorporate modules and bundle them for frontend projects. Later, the community introduced more powerful bundling solutions like Webpack and Parcel, allowing the process to be a regular part of modern JavaScript development.

If you want to delve deeper into the details of the history of JavaScript modules, see this article.

Native JavaScript modules

Solutions like CommonJS were necessary to make modules possible in JavaScript (particularly in Node.js). But JavaScript needed something better. In 2015, the ECMAScript standard officially added support for native modules (or ES Modules) that would no longer require third-party tooling for modular development in the browser.

Native modules are beneficial for several reasons, including:

  • JavaScript modules allow developers to encapsulate and organize code into smaller, reusable parts. This modularity makes it easier to manage and maintain any codebase.
  • You can reuse JS Modules, which avoids duplication in a project and helps keep code DRY (i.e., “Don’t Repeat Yourself”).
  • Modules ensure that the browser loads only the necessary parts of the code at any given time, thus improving the performance of web apps, including PWAs.
  • Modular code largely prevents naming conflicts since modularity isolates variables and other references, thus preventing conflict with other modules and not polluting the global scope.
  • Modules make unit testing much easier, since you can run tests on isolated modules without affecting other pieces of code.

With that small history lesson and the benefits made clear, let’s now dig into the code so you can see how to use JS Modules in modern web apps.

Syntax for including JavaScript modules

You can insert any JavaScript module into a web page using almost the same syntax as any other script. But note one small difference:

<body>
  ...

  <script src="index.js" type="module"></script>
</body>
Code language: HTML, XML (xml)

In the above code, I’ve added the <script> element to the bottom of my HTML page. I’m referencing the script as I normally would using the src attribute. But, notice the type attribute. Instead of the customary value of text/javascript, I’ve used a value of module. This tells the browser to enable JavaScript module features in this script rather than treating it like a normal script.

How does a script differ when it’s included as a module?

  • The browser automatically defers scripts with type="module", so using the defer attribute would not effect this script.
  • Modules are subject to CORS rules, meaning you can access modules only on the same domain or those on a different domain that you’ve allowed permission to access via CORS. This is unlike regular scripts that you can load from anywhere.
  • The browser interprets individual modules in strict mode.

In the example above, I can consider the index.js file as the entry point of my modular application. I could call this file whatever I want, but many developers customarily use index.js as their app entry point.

Now, I’m going to create some modules I’ll use inside index.js. Here’s a look at my folder structure so you can get an idea of how I’m organizing my modules:

index.html
index.js
modules/
    one.js
    two.js
    three.js

Your own app’s folder structure and file/folder names might differ, but the above should be easy enough to understand for demo purposes here. The index.html file would be the one that includes the <script> element that inserts the initial module entry point (index.js).

Notice I’ve added a folder called modules, which will store all of my modules in separate files. In this case, there are three modules.

Syntax for importing and exporting JavaScript modules

Now that I have a module entry point and I’ve set up my folder structure, I’ll show you how you can import any of the modules into the main script.

The one.js file is going to hold the following simple module script:

export function add(a, b) {
  return a + b;
}
Code language: JavaScript (javascript)

This is nothing but a simple add() function. The key portion of the script that makes this a usable module is the use of the export statement. This syntax is only usable inside a module. With this in place, I can then import the module inside main.js like this:

import { add } from './modules/one.js';

console.log(add(5, 8)); 
Code language: JavaScript (javascript)

The first line of code is where I import the add() function, wrapped in curly braces. I then define from where I want to import the module using the “from” keyword, followed by the module’s location in quotes.

Once I have the module imported, I can use the add() function as I would any function that I have access to. In this instance, I’m calling the function and passing in two numbers, which get added, and the function returns the value.

I don’t have to export a value at the same time that I define it. For example, earlier, I exported the add() function by putting the export keyword ahead of the function keyword. I could alternatively do:

function add(a, b) {
  return a + b;
}

export { add };
Code language: JavaScript (javascript)

Here I’m exporting the reference to add()rather than the declaration of add(). Note the use of curly braces around the add reference, which is required. The same principle would apply to anything I’m exporting – functions, variables, or classes. I can export the reference rather than the declaration.

That’s the simplest way to explain JavaScript modules syntax. There are quite a few other aspects to the code, so I’ll cover those next.

Syntax for multiple exports in a JavaScript module

In a JavaScript module file, I can export any number of values, including anything stored in a variable, a function, and so on. To demonstrate, I’ll add the following code to my two.js module:

export let name = 'Sally';

export let salad = ['lettuce', 'tomatoes', 'onions'];
Code language: JavaScript (javascript)

I’ve exported a simple name variable followed by an array called salad. Now I’ll import them, so my index.js file will look like this:

import { add } from './modules/one.js';
import { name, salad } from './modules/two.js';

console.log(add(5, 8)); 
console.log(name); 
console.log(salad[2]); 
Code language: JavaScript (javascript)

Take note of the syntax required for importing multiple values. I’ve placed the name and salad references inside the curly braces and separated them with a comma. This list of comma-separated imports could be any number of references, as long as I properly export them from the two.js file.

Another way I can import multiple exports from a single file is by using the asterisk character during my import. Let’s say I have the following in my module:

export function add(a, b) {
  return a + b;
}

export let num1 = 6,
           num2 = 13,
           num3 = 35;
Code language: JavaScript (javascript)

Notice I’m exporting a function along with three different variables. I can import all of these exports using the following syntax:

import * as mod from './modules/one.js';

console.log( mod.add(3, 4) ); 
console.log( mod.add(mod.num1, mod.num2) ); 
console.log( mod.num3 ); 
Code language: JavaScript (javascript)

Notice I’m exporting the entire module as an object called mod. From there, I can access all the functions, properties, or classes defined in the mod object using the familiar object dot notation.

In any of these examples, once I import the exports, I can then use them however I like in my application code.

Renaming JavaScript module exports

I can rename a module’s export before it’s exported, using the as keyword, as in the following example:

function add(a, b) {
  return a + b;
}

export { add as addFunc };
Code language: JavaScript (javascript)

With that code inside the module, importing would look like this:

import { addFunc } from './modules/one.js';
Code language: JavaScript (javascript)

This allows me to use a different name for the export when it’s used inside the main application code compared to how it’s named in the module itself. I just have to make sure I reference the function as addFunc() when I use it.

I can also use the renaming syntax when doing the import. Assuming I’ve exported the function with its original name, add, I can do the following:

import { add as addFunc } from './modules/one.js';
Code language: JavaScript (javascript)

This is the same basic idea as the previous example; I’m just doing the rename on import rather than on export. Of course, you’d have to be careful when renaming so as not to cause confusion in the code. You should generally have a good reason for renaming the export to make sure the code is still readable and maintainable.

Exporting defaults in JavaScript modules

The way I’ve exported and imported parts of my module code in earlier code examples is using what’s referred to as a named export. The other kind of export is a default export. Exporting a default value from a module is a pattern carried over from third-party module systems that I mentioned earlier. This became part of the ECMAScript standard to be interoperable with those older tools.

I can define a default export as follows:

export default function add(a, b) {
  return a + b;
}
Code language: JavaScript (javascript)

This is the same function I exported earlier, except this time I’m using the default keyword after the export keyword. I can also export a default value by reference:

function add(a, b) {
  return a + b;
}

export default add;
Code language: JavaScript (javascript)

And I can use the renaming syntax:

function add(a, b) {
  return a + b;
}

export { add as default };
Code language: JavaScript (javascript)

In the case of the renaming syntax, I’m simply using the keyword default in place of the export name.

To import any of the above default values, I can do the following:

import add from './modules/one.js';
Code language: JavaScript (javascript)

Notice I’m not using the curly braces around the import name. Non-default exports require curly braces, whereas a default export has no curly braces. Also, I can import any of the above exports using the following:

import addFunction from './modules/one.js';
Code language: JavaScript (javascript)

In this case, I’ve renamed the import. Because this is a default export, I can alter the name as I import it; I’m not restricted to using the exported name. This is also clear from the fact that the as syntax didn’t use a custom name when I exported the default.

The other thing that’s important to understand about default exports is that I can export only one value as the default. So, if I have multiple values I want to export in a module, I would use a similar syntax to the one earlier when I exported multiple items with an asterisk.

Here is my module:

export function add(a, b) {
  return a + b;
}

export let num1 = 6,
           num2 = 13,
           num3 = 35;
Code language: JavaScript (javascript)

And here is my index.js file:

import mod from './modules/one.js';

console.log( mod.add(3, 4) ); 
console.log( mod.add(mod.num1, mod.num2) ); 
console.log( mod.num3 ); 
Code language: JavaScript (javascript)

The main difference here is that I’m not using the asterisk or the as keyword; I’m simply importing the entire module and then working with it as I would any object.

More tips and facts on JavaScript modules

What I’ve discussed so far covers most of the basics to get you up and running with ES6 modules. Once you get past the basics, there are different subtleties you’ll want to keep in mind as you write your modules, which I’ll cover in this section.

Understanding encapsulation

Firstly, just because some code exists in a module doesn’t mean it’s going to be accessible in your primary script (the one that imports modules). For example, let’s say three.js has the following code:

function subtract (c, d) {
  return c - d;
}

export function add (a, b) {
  return a + subtract(a, b);
}
Code language: JavaScript (javascript)

I can then import and use the add() function, but notice what happens if I try to use the subtract() function:

import { add } from './modules/three.js';
import { subtract } from './modules/three.js';


console.log(add(23, 16)); 
console.log(subtract(30, 34)); 
Code language: JavaScript (javascript)

Notice I can’t import the subtract() function, nor can I use it. The subtract() function is part of my module’s logic and is necessary for the module to work. But it’s not an exported function, so I don’t have access to it outside of the module unless I explicitly export it.

Relative path names

As shown in multiple examples above, I can import JavaScript modules by referencing a JavaScript file. But notice what happens if I try to use the following syntax:

import { add } from 'modules/three.js';

Code language: JavaScript (javascript)

I can use an absolute file path (i.e., a full URL), and that would be no problem (as long as it passes CORS requirements). But if I’m using a relative file reference, the path needs to include a form that has a forward slash at the beginning of the path. This could be any one of the three formats shown in the error message above.

Strict mode in modules

Each encapsulated module works in the same way that code in strict mode works (that is, code in a block that has a 'use strict' statement at the top). So whatever rules apply to strict mode, the same rules apply to code in a module. For example, the value of this in the top level of a module is undefined, which isn’t the case when not in strict mode.

This means you can reference this at the top level inside the file doing the importing or inside any of the modules and the result will be the same: undefined.


function add(a, b) {
  return a + b;
}

console.log(this); 
export { add };
Code language: JavaScript (javascript)

This is different from regular scripts in the browser, where this at the top level is a reference to the Window object.


function add(a, b) {
  return a + b;
}

console.log(this); 
export { add };
Code language: JavaScript (javascript)

Implied const in modules

Another point to keep in mind is that anything I import behaves as if I defined it using const. If you’re familiar with const, this is a way to declare a variable that I can’t change (unless it’s an object, in which case I can change the properties).

To illustrate, suppose my two.js file contains the following:

export let name = 'Sally';
Code language: JavaScript (javascript)

I’m using let to define the variable that’s exported. But notice what happens if I try to change it after import:

import { name } from './modules/two.js';

console.log(name); 
name = "Jimmy";

Code language: JavaScript (javascript)

Even though I didn’t use const, the code import behaves as if I did.

Exporting and importing limitations

When doing imports or exports, I have to have them outside of other statements and functions. For example, the following code inside a module would throw an error:

if (name === 'Sally') {
  export { name };
}

Code language: JavaScript (javascript)

The same would result if trying to export inside of a function body.

Importing multiples using default and non-defaults

As mentioned, you can import only one default. But this doesn’t mean you’re limited to importing a single value from a module. For example, here’s my one.js file:

function add(a, b) {
  return a + b;
}

export let name = "Sally";
export { add as default };
Code language: JavaScript (javascript)

Notice I’m exporting the add() function as the default, but I’m also exporting a variable.

Now here’s index.js:

import add, { name } from './modules/one.js';

console.log(name); 
console.log(add(10, 8)); 
Code language: JavaScript (javascript)

I import the default with no curly braces and I export the variable with curly braces, and both are accessible as expected. When combining the default with non-default imports, I have to list the default first; otherwise, it will throw an error.

The exception would be if I were renaming the default, then I would put both inside the curly braces:

import { default as addFunc, name } from './modules/one.js';

console.log(name); 
console.log(addFunc(10, 8)); 
Code language: JavaScript (javascript)

Using the .mjs file extension for JS Modules

One final thing I’ll mention here is that with JavaScript modules, you have the option to use a file extension of .mjs instead of .js for your module files. This can help with maintainability and how the modules work with some tools.

However, there are a few caveats, which you can read about in MDN’s reference.

Wrapping up this tutorial

That wraps up this tutorial on JavaScript modules. There’s more I could talk about, including how modules play an important role in your build tools process and how these tools will minify your modules. But this should be enough to give you a basic framework from which to get started.

Feel free to use the code examples from here to create your own testing ground to try out these features. This way, you can work with some live examples in your personal development environment and become more familiar with how this JavaScript standard works in practice.

Wp-dd.com

WordPress Design, WordPress Development, cPanel Hosting, Web Design, Web Development, Graphic Design, Mobile Development, Search Engine Optimization (SEO) and more.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.