DEV Community

Cover image for A Complete Beginners Guide to JavaScript Modules (2023 Noobs are Welcome)
Sojin Samuel
Sojin Samuel

Posted on • Edited on

A Complete Beginners Guide to JavaScript Modules (2023 Noobs are Welcome)

If you're new to JavaScript, terms like "module bundlers vs. module loaders," "Webpack vs. Browserify," and "AMD vs. CommonJS" might be confusing.

Although the JavaScript module system might seem frightening, web developers must grasp it.

In this piece, I'll explain these buzzwords in plain English (and a few code samples). I hope it is useful to you!

Please keep in mind that this will be separated into two portions for ease of reading: Part 1 will go over what modules are and why we utilize them. Part 2 (to be published next week) will explain what it means to bundle modules and how to do it in various ways.

Part 1: Could someone simply explain modules to me again?

javascript modules simplified

Good authors organize their novels into chapters and parts, whereas good programmers organize their programs into modules.

Modules, like book chapters, are just groups of words (or code, as the case may be).

Good modules, on the other hand, are extremely self-contained and have separate functions, allowing them to be shuffled, deleted, or added as needed without disturbing the overall system.

Why should you utilize modules?

Why should you utilize javascript modules

There are several advantages to using modules instead of a huge, interconnected codebase. The following are, in my opinion, the most important:

1) Maintainability: A module is, by definition, self-contained. A well-designed module strives to reduce its reliance on other portions of the codebase as much as possible, allowing it to grow and improve independently. When a module is separated from other parts of code, updating it becomes considerably easier.

To return to our book example, if you wanted to update a chapter, it would be a nightmare if a little modification to one chapter forced you to make changes to every other chapter as well. Instead, you should write each chapter such that changes may be done without impacting previous chapters.

2) Namespacing: Variables beyond the scope of a top-level function are global in JavaScript (meaning, everyone can access them). As a result, "namespace pollution" occurs often, in which entirely unrelated code shares global variables.

Sharing global variables between unrelated programs is a development no-no.

Modules, as we'll see later in this piece, help us to prevent namespace pollution by providing a private place for our variables.

3) Reusability: Let's be honest: we've all cloned code from prior projects into new ones at some time. For instance, suppose you cloned certain utility methods from a prior project to your present project.

That's OK, but if you discover a better method to write a portion of that code, you'll have to go back and remember to change it everywhere else you wrote it.

This is clearly a waste of time. Wouldn't it be much easier if there was a module that we could reuse again and over?

How may modules be incorporated?

Modules may be integrated into your programs in a variety of ways. Let's go through a couple of them:

Module design

The Module pattern is used to imitate the notion of classes (since JavaScript does not natively support classes) so that we may contain both public and private functions and variables inside a single object, similar to how classes are used in other programming languages such as Java or Python. This allows us to design a public-facing API for the methods we wish to expose to the rest of the world while keeping enclosing private variables and methods in a closure scope.

There are various approaches of implementing the module pattern. I'll use an anonymous closure in this first example. By placing all of our code in an anonymous function, we'll be able to achieve our aim faster. (Remember that functions are the only method to create new scopes in JavaScript.)

Exemplification 1: Anonymous closure

anonymous function sojin samuel

With this approach, our anonymous function has its own evaluation environment, or "closure," and we evaluate it instantly. This allows us to conceal variables in the parent (global) namespace.

What's good about this method is that you can utilize local variables within this function without mistakenly overwriting existing global variables, while still accessing them, as seen below:

sojin samuel

The parenthesis surrounding the anonymous function are essential since statements that begins with the keyword function are always considered function declarations (remember, nameless function declarations are not allowed in JavaScript). As a result, the parenthesis around it generate a function expression instead. If you're interested, you can find out more here.

Exemplification 2: Global import

Global import is another popular technique used by libraries such as jQuery. It's identical to the anonymous closure we saw earlier, but we now send in globals as parameters:

Global import

The only global variable in this example is globalVariable. The advantage of this method over anonymous closures is that you declare the global variables upfront, making it obvious to anybody reading your code.

Exemplification 3: Object interface

Another way is to develop modules with a self-contained object interface, as seen below:

Object interface

As you can see, this technique allows us to choose which variables/methods to keep private (e.g., myGrades) and which variables/methods to disclose by including them in the return statement (e.g. average & failing).

Exemplification 4: Disclosing the module pattern

This solution is fairly similar to the last one, only it guarantees that all methods and variables are kept private until expressly exposed:

Disclosing the module pattern

That may appear to be a lot to take in, but it's only the tip of the iceberg in terms of module patterns. Here are a few resources I found useful in my own research:

  • Learning JavaScript Design Patterns by Addy Osmani: a wealth of information in an amazingly brief read
  • Ben Cherry's Adequately Good: a useful overview with examples of advanced module pattern usage.
  • Carl Danley's blog: description of the module pattern and links for additional JavaScript patterns.

AMD and CommonJS

The options described above have one thing in common: they all employ a single global variable to wrap their code in a function, so creating a private namespace for themselves via a closure scope.

While each strategy is efficient in its own way, it has drawbacks.

For starters, as a developer, you must understand the proper dependency order in which to load your files. Assume you're using Backbone in your project and add the script tag for Backbone's source code in your file.

However, because Backbone has a strong reliance on Underscore.js, the Backbone script tag cannot be inserted before the Underscore.js file.

Managing dependencies and doing these things properly may be difficult as a developer.

Another disadvantage is that they can still cause namespace conflicts. What if two of your modules share the same name, for example? What if a module has two versions and you require both?

So you're probably thinking whether we can create a mechanism to request a module's interface without using the global scope.

Thankfully, the answer is yes.

CommonJS and AMD are two prominent and widely used techniques.

CommonJS

CommonJS is a volunteer working group that creates and implements JavaScript APIs for module declaration.

A CommonJS module is simply a reusable piece of JavaScript that exports certain objects that other modules may use in their projects. If you've worked with Node.js before, you'll be extremely comfortable with this format.

Each JavaScript file in CommonJS contains modules in its own unique module context (just like wrapping it in a closure). In this scope, we expose modules and require them to be imported using the module.exports object.

When creating a CommonJS module, you could see something like this:

CommonJS module

We employ the special object module and include a reference to our function in module.exports. This informs the CommonJS module system of what we wish to expose so that other files can use it.

When someone wishes to utilize myModule, they may include it in their project as follows:

utilize javascript module

This technique has two clear advantages over the module patterns we mentioned previously:

  1. Keeping global namespace pollution to a minimum
  2. Making our interdependence clear

Furthermore, the syntax is fairly concise, which I personally appreciate.

Another thing to keep in mind is that CommonJS is server-first approach and loads modules synchronously. This is significant because if we have three further modules that we need to load, it will do so one by one.

That works wonderfully on the server but makes it difficult to utilize when developing JavaScript for the browser. To summarize, reading a module via the web takes significantly longer than reading from disk.

The script used to load a module prevents the browser from doing anything else until it is finished loading. This is because the JavaScript thread does not stop until the code is loaded. (I'll go through how we can get around this in Part 2 when we talk about module bundling.) That's all we need to know for the time being).

AMD

CommonJS is great, but what if we need to load modules asynchronously? The solution is known as Asynchronous Module Definition, or AMD for short.

Using AMD to load modules This is what it looks like:

AMD to load module

The define function accepts as its first parameter an array containing each of the module's dependencies. These dependencies are loaded in the background (non-blockingly), and after loaded, define calls the callback function that was provided.

Following that, the callback function accepts the dependencies that were loaded as arguments — in our example, myModule and myOtherModule — allowing the function to utilise these dependencies. Finally, using the define keyword, the dependencies themselves must be declared.

For instance, myModule may look like this:

javascript myModule

So, unlike CommonJS, AMD uses a browser-first strategy in conjunction with asynchronous behavior to complete the task. (Note that many people feel that dynamically loading files piecemeal as you begin to run code isn't a good idea, which we'll discuss further in the following section on module-building.)

Aside from asynchronicity, another advantage of AMD is that your modules may be of any type, including objects, functions, constructors, strings, JSON, and many more, whereas CommonJS only allows objects as modules.

However, AMD is incompatible with CommonJS's io, filesystem, and other server-oriented capabilities, and the function wrapping syntax is a little more verbose than a simple need statement.

javascript module amd

Check out this fascinating GitHub project for more examples of UMD formats.

Native JavaScript

Phew! Are you still alive? I didn't lose you in the woods here, did I? Good! Because we still need to specify one more type of module before we're finished.

As you may have noted, none of the modules mentioned above were native to JavaScript. Instead, we've developed methods to simulate a modules system utilizing the module pattern, CommonJS, or AMD.

Fortunately, the wise people at TC39 (the standards organization that governs ECMAScript syntax and semantics) have added built-in modules in ECMAScript 6. (ES6).

ES6 has a number of options for importing and exporting modules, which others have thoroughly explained – here are a couple of those resources:

What distinguishes ES6 modules from CommonJS or AMD is their ability to provide the best of both worlds: succinct and declarative syntax, asynchronous loading, and additional benefits such as improved support for cyclic dependencies.

Imports are live read-only views of the exports, which is probably my favorite feature of ES6 modules. (Contrast this with CommonJS, where imports are copies of exports and hence not alive.)

Here's an illustration of how it works:

Native JavaScript modules

In this example, we make two copies of the module: one when we export it and one when we use it.

Furthermore, the copy in main.js is no longer linked to the original module. Because the counter variable we imported is an unconnected duplicate of the counter variable from the module, even when we increment it, it still returns 1.

In other words, incrementing the counter will increment it in the module but not in your duplicated version. The only way to change the cloned version of the counter variable is by hand:

javascript modules sojin samuel

In contrast, ES6 generates a live read-only representation of the modules we import:

javascript modules simplified

Isn't it cool? What I like best about live read-only views is that they allow you to divide your modules into smaller chunks without sacrificing functionality.

Then you may easily reverse the process and integrate them again. It simply "works."

Module bundling in the future

Wow! What happened to time? That was a crazy voyage, but I genuinely hope it helped you understand modules in JavaScript better.

In the next part, I'll go through module bundling, covering key concepts such as:

  • Why do we group modules together?
  • Various techniques to bundling
  • The module loader API in ECMAScript
  • ...and more. :)

PS: In order to keep things simple, I glossed over some of the finer points (think: circular dependencies) in this tutorial. Please let me know if I missed something crucial or intriguing in the comments!

Support me and Be my Twitter Buddy

Sojin out!!

Top comments (0)