Demystifying JS Modules

Hello! Welcome. It's great to have you here!!

Before we unpack how JS Modules work, let us first understand why we need modules in the first place.

What are the problems with just using the native JS files?

Everything is fine when we have a single JS file but problems start to arise when we have multiple JS files. And when we are developing applications, we segregate code into files depending on the functionality.

When we load multiple JS files in the script tag, the contents of them are just concatenated.

Consider the following JS files: add1.js and add2.js

// add1.js
function add(a, b) {
    return a + b;
}
// add2.js
var add = "addString";

Now if we load both scripts, they will be concatenated


<script src="add1.js">
<script src="add2.js">

<!-- Above script tags will be translated
    as follow
!--> 

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

    var add = "addString";

</script>

And when it executes, the "add function" will be overwritten with "addString". This is an example of a name collision issue which occurs due to the presence of the same identifier name in the same scope (in our case -> the global scope).

Let us have a look into another issue. i.e Global scope pollution

Here we have an index.html file which loads main.js

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <script src="main.js" defer> </script>
</head>
<body>   
</body>
</html>
// main.js
var name = "Hiesenberg";
function sayMyName(name) {
    console.log(`Hello ${name}`);
}

sayMyName(name);

When a JS file is run inside a browser, all the global identifiers [ those which are not inside of any function ] are available on the window object ( a global object ) as a property. Open index.html in the browser and the navigate to console section in the devtools.

console.log(window.name) // Hiesenberg

So whenever we create a global identifier, we are modifying the global window object provided to us by the browser, and if our identifier name matches with any of the properties of the window object, we would be overwriting that property, which will lead to bugs in our program.

var alert = "My alert";
window.alert("Show me the alert");

// Now if we run this code, we would be getting an typeError saying window.alert is not a function, because we have overwritten the alert function which the global window object provides.

Now that we have an understanding of what problems, let us see how we can remediate them.

The problem arises because we are directly modifying the global scope, we need to create an isolated scope for each JS file.

Is there any way we can encapsulate the contents of a specific file?

Is there any other scope available to us other than the global scope?

Yes, the function scope.

So what we can do is wrap all the content of a file inside a function. And we still have to call the wrapper function to execute the code inside.

// main.js
function mainJSWrapper() {
    var name = "Hiesenberg";
    function sayMyName(name) {
        console.log(`Hello ${name}`);
    }
    sayMyName(name);
}

// calling the wrapper function
mainJSWrapper();

Here, we have scoped the contents of main.js file to mainJSWrapper function.

Now if we try something like

window.name // undefined
window.sayMyName // undefined

window.mainJSWrapper // function (OOPS!!! We exposed this function to global window object)

So, we managed to encapsulate the contents of main.js file, but in doing so we exposed the mainJSWrapper function globally.

What is the issue? / The problem here is

We are separating the steps for declaring the wrapper function and calling the wrapper function, which results in exposing the wrapper function to the global window object.

What if we could combine these two? What if we could immediately invoke a function right after we declare it?

Yes, that's right. The solution is IIFE ( Immediately Invoked Function Expressions ).

(function mainJSWrapper() {
    var name = "Hiesenberg";
    function sayMyName(name) {
        console.log(`Hello ${name}`);
    }
    sayMyName(name);
})();

Thus, by using IIFE we can provide isolated scope for each JS file.

This is what a module looks like in JS.

Modules in JS are implemented on a per-file basis. So what a module does, is it wraps the JS code inside an IIFE providing an enclosing scope.

But a module system is not just limited to creating an enclosing scope, we also need a way to communicate between different modules.

Before modules were part of official ECMA specifications, various people proposed their implementation of what a JS module system would look like.

Let us look at one of them called CommonJS module specification which is used in Nodejs.

By using CommonJS, we achieve the following:

  1. We can share variables and functions among modules.

  2. Each module has its separate scope.

Consider the following modules add.js and main.js which have implemented the CommonJS specification.

**The following code needs to run on Nodejs, browsers do not support the commonJS specification.

// add.js
function add(a, b) {
    return a + b;
}
// either we can use module.exports
module.exports.add = add
// or we can use just exports
exports.add = add;
// main.js
const add = require('./add.js');
console.log(add.add(1, 3)); // 4

To share the add function between the modules, we can use either module.exports object or exports object. When we define any property on these export objects, they become available to any module which requires them.

Now you may be wondering how does this all work? How does the require function returns what is available on the module.exports object and from where does this module.exports object comes from?

require is a function provided to us. What is happening behind the scene is the contents of the filename passed in to require function are loaded inside a function which gets passed in module, exports as arguments and the function return the module.exports object.

So what happens when we require('./add.js') is like


function wrapper(module, exports) {
    //contents of the add.js
    function add(a, b) {
        return a + b;
    }
    module.exports.add = add

    return module.exports;
}


// Then we call the function passing the arguments like
const module = {
    exports: {}
}
wrapper(module, module.exports);

// And finally this module.exports objects is returned to us by the require function

**A few details on module.exports and exports

When a CommonJS module is loaded, a new module object is created, which has an exports property initialized to an empty object, and a module.exports property initialized to exports. This means that initially, module.exports and exports both point to the same object.

You can use either module.exports or exports to add properties and methods to the module's exports object. However, if you reassign exports to a new value, it will no longer reference the same object as module.exports. This means that any changes made to exports after that will not be reflected in the module's exports.

ES Modules

After some time, Javascript natively started supporting modules known as ES Modules in 2015. It took almost 10 years for the same.

ES Modules work a bit differently from CommonJS modules but they adhere to the core principles of a module system.

We have to explicitly specify the type in the script tag to let the browser know to treat it as a module instead a simple JS file.

<script src="main.js" type="module"> </script>

What next?

Now that the module system is supported in the browser, can we write separate codes in respective modules and ship them over as follows?

<script type="module" src="module1"></script>
<script type="module" src="module2"></script>
<script type="module" src="module3"></script>
.....
.....

Adding multiple script tags will queue multiple requests over the network. This will result in poor performance of the web application.

And that is why we need something called a module bundler, which combines multiple modules into a minified asset which will be sent to the browser. And that is how modern frameworks like React, and Angular work.

Hope, this blog added value to your learning. Any feedback or suggestions are most welcome.

In the next one, we would be going into how exactly a module bundler works and how we can create a simple bundler from scratch.

Thank You!!