Understanding Modules in Node.js

I’ve encountered the term Immediately Invoked Function Expression (IIFE) a ton while preparing for my interviews. Initially it was very frustrating — every time I googled something like “Top 100 JavaScript Interview Questions” the topic would come up, but I could never find enough real-life implementations to understand the use cases.

For anyone who isn’t familiar with the term, an IIFE is just a way to create a module in JavaScript. To fully understand modules and why they’re useful, we first need to understand global scope and why it behaves differently in a file than it does in a browser.

Global Scope


Before we start, it’s important to double-down on the distinction that global scope works differently in the browser than it does in a file with a .js extension. I’ll do my best to clarify the differences by labeling the example images with their respective environment

Similarities between global scope in the browser and in a file

Every JavaScript program has a global object that contains useful properties and methods.

In the browser, this global object is referred to as the window.

In a Node.js file, this global object is simply referred to as global.

You use the global object all the time, frequently by accessing the .log method attached to the global object’s console property.

Fortunately, methods and properties that belong to the global scope can be referenced directly. So while it’s possible to log something to the console with window.console.log in a browser or global.console.log in a Node.js file, we can just make a direct call to the console property.

Differences between global scope in the browser and in a file

The best way to illustrate the difference in browser and file global object behavior is to simply create some variables and functions. We’ll use a constant name variable and a sayName function in both the browser and a .js file.

In the browser, any variable or function that we declare gets added to the global window object. This can lead to some weird behavior.

A feature of Node files with the .js extension is that variables and functions we define in files are not attached to the global object. Instead, they are scoped locally to the file in which they were declared — meaning that files with the .js extension are modules themselves.

The main benefit of .js files (and therefore modules) is that they allow us to encapsulate our code into private collections of variables and functions. The variables and functions defined within a file won’t pollute the global scope and can only be made available to other modules if we choose to expose them. The reason why this works is because all .js files that are run with Node.js are immediately wrapped with an IIFE at runtime, giving them their own local scope. We’ll explain this in more depth in the next section.

Alternatively in the browser, it’s easy to envision a scenario where we accidentally overwrite one of the window object’s built-in methods or properties, which could lead to a myriad of annoying side effects.

Privatizing Data & Methods

The best way to illustrate how IIFEs work is to build one from the ground up. First, we create an anonymous function. This anonymous function can be in the form of a nameless function declaration…

… or if you prefer ES6 syntax, you can use an anonymous arrow function.

Next, we wrap the anonymous function in a set of parentheses.

What does wrapping our anonymous function in parentheses do? It converts our function into an expression.

What’s special about an expression? An expression in JavaScript is a unit of code that evaluates to a single value. In our case, wrapping our anonymous function in parentheses creates an expression that evaluates to a single function object.

Since our new parentheses create an expression that evaluates to a function, we simply need to invoke that function to complete our IIFE.

Our FUNCTION is wrapped with parentheses (making it an EXPRESSION) and is IMMEDIATELY INVOKED by the second pair of parentheses.

When we invoke an IIFE, what we’re really doing is we’re invoking the anonymous function inside the IIFE. Since the anonymous function has its own private function scope, we can add variables and functions to the IIFE without having to worry about them affecting other parts of the program.

Let’s give each of our IIFEs a variable.

One thing to notice is that we’ve defined the constant variable greeting twice, once per IIFE. Since each IIFE creates its own function scope, the greeting variable has different meanings depending on context.

We can play by the same rules with functions inside of an IIFE.

Since both IIFEs have two different function scopes, their respective sayHello functions are going to log different greeting values to the console.

To help conceptualize things, you can think of each IIFE as being its own separate file. Just like the .js files we described before, they have their own local scope and can be given private functions and variables that will only be accessible by other IIFEs if we choose to expose them.

Therefore, the two IIFEs we’ve created in this single index.js file…

Is basically the same thing as having both IIFE’s in two separate files

Exposing Data and Methods

To publicly expose functions and data that other IIFEs might find useful, we simply use a return statement to return an object containing the functions and data that we want to make public.

The connection we want to make is how similarly this behaves to module.exports, which you’ve probably seen many more times than IIFEs if you have any experience in Node or React.

Since IIFEs have modular functionality and files with .js extensions are actual modules themselves, let’s again split our code into two separate .js files.

Remember that earlier we said .js files are wrapped with an IIFE at runtime, so we would be nesting IIFEs inside of IIFEs with our current setup. Let’s get rid of the IIFEs…

… and replace with the return statements with module.exports

Let’s say that the first file (first.js) wants to borrow some functions that were defined in the second file. Let’s follow the flow of how that would work.

First, the module we’re importing from needs to have defined the function that we want to import.

Secondly, we need to make sure that the function has been exposed by the module with module.exports

In the file where we want to import the functionality, we simply call the require() function on the file path that points to the file where the exports are.

The require function finds the exports and assigns them to variables. In our case, we used ES6 destructuring to store the exports.

The first file now has access to the imported exports (??) from the second file and can evoke those imported functions as needed.

Now that the first file has access to the exports from the second file, the first file can now use those functions just like the second file can.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store