In our coverage of Object-Oriented Programming (OOP) concepts in JavaScript, it’s now time to explore code organization with modules. While not directly related to OOP, it’s common to store classes in separate files and import them as needed, enhancing code organization, modularity, and maintainability. This is where JavaScript modules come in.
Modules encapsulate related functionality and data, making our code more modular, reusable, and maintainable. Introduced in ES6 (ECMAScript 2015), they represent a relatively new feature in JavaScript.
ES6 modules are stored in separate files, with each file representing a module. A module can contain classes, functions, variables, and other code that can be exported and imported into other modules. This separation of code into modules helps to organize and structure our codebase.
Consider a simple script.js
file that contains variable declaration:
Suppose this script is linked to an HTML file:
The variable pi
is now available in the global scope. If you open the HTML file in a browser and then open the browser’s console, you can access the variable number
:
In order to avoid polluting the global scope, we can use modules to encapsulate our code. This is particularly useful when working with larger codebases. To that aim, all you need to do is add the type="module"
attribute to the script tag:
Now, the variable pi
is no longer available in the global scope. If you try to access it in the browser’s console, you’ll get an error:
To make the variable pi
available in other parts of your code, you need to export it from the module. We will cover this shortly.
In Node.js, modules are used to organize code into separate files. Each file is treated as a separate module, and you can export and import functionality between modules. However, Node.js uses the CommonJS module system by default, which is different from the ES6 module system used in the browser.
In order to use ES6 modules in Node.js, you need to add the property "type": "module"
to your package.json
file. This tells Node.js to treat all JavaScript files as ES6 modules. For example, your package.json
file might look like this:
When you create a module in JavaScript, it creates a new scope for the code within that module. This means that variables and functions defined in a module are not accessible outside the module unless explicitly exported. This helps to prevent naming conflicts and makes it easier to reason about your code.
To make variables, functions, or classes available outside a module, you need to export them. You can export individual items or export everything at once. Here’s is an example:
In the example above, we export the INTEREST_RATE
constant and three functions: deposit
, withdraw
, and getBalance
. These items can now be imported into other modules (whereas balance
remains private to the module).
Alternatively, you can export everything at once using the export
keyword:
You can also give an alias to an exported value with the as keyword:
Moreover, you can break a long export statement into several export statements (not a very common practice):
To use items exported from another module, you need to import them. Here’s how you can import the three functions from the account.js
module:
Notice that there is no requirement to import everything from a module. You can import only the items you need.
When it comes to importing, you need to follow these rules:
import
keyword, followed by the items you want to import enclosed in curly braces {}
.{}
.from
keyword is used to specify the path to the module you are importing from. The path can be relative (to where you import) or absolute.Similar to exports, you can use aliases when importing:
It is possible to use multiple import statements to import values from the same module:
It is also possible to import everything from a module using the *
wildcard:
The imported values are stored in an object named Account
, and you can access them using dot notation.
It is important to note that modules are not types. When you import a module, you are importing the values that the module exports. The module itself is not a type. In our earlier example, there is only ever one instance of Account. Therefore, you cannot create different types of accounts, each having its own “balance.”
To use Account
as a type, you would need to create a class that defines the structure of an account. This is a common pattern in object-oriented programming, where classes are used to define types.
We can now import the Account
class and use it to create different account instances:
In summary, use classes to define types and modules to organize and encapsulate related functionality and data. The module and class constructs have a similar goal of creating encapsulated units of code, but they serve different purposes. Modules are used to organize code into separate files, while classes are used to define types. By combining modules and classes, you can create a well-structured and maintainable codebase.
In addition to named exports and imports, ES6 modules also support default exports and imports. Default exports are used to export a single value from a module, while default imports are used to import that value. Here’s an example:
In the example above, we export the Account
class as the default export. This means that when you import the module, you can choose to import the default export without using curly braces. Here’s how you can import the default export:
You can also combine default and named exports in the same module:
When using default exports, you can give the default export a name when importing it:
As discussed earlier, modules help to organize and encapsulate related functionality and data. Before the introduction of modules in ES6, JavaScript did not have a built-in module system. In the early days, this was not a problem because JavaScript was primarily used for simple tasks like form validation and DOM manipulation. However, as JavaScript applications grew in size and complexity, the need for a more structured way to organize code became apparent. Therefore, the JavaScript community developed various patterns to encapsulate code.
One such pattern is the Immediately Invoked Function Expression (IIFE). An IIFE is a function that is defined and immediately invoked.
Here is the syntax for an IIFE:
It contains two major parts:
(function () { ... })
.()
immediately following the function expression.Consider the early example of a simple script.js
file that contains variable declaration:
To encapsulate the variable pi
within an IIFE, you can do the following:
This would prevent the variable pi
from being available in the global scope. In this particular case,
the script does nothing useful, but it demonstrates how an IIFE can be used to encapsulate code. Here is an slightly modified version which prints the value of pi
:
When you run the script, you will see the value of pi
printed to the console. But if you try to access the variable pi
in the global scope, you will get an error!
IFFEs were a common pattern used to encapsulate code before the introduction of modules in ES6. While they are still useful in certain situations, modules provide a more structured and standardized way to organize and encapsulate code.
Before ES6 modules, the CommonJS module system was widely used in Node.js. CommonJS modules are synchronous and use require
to import modules and module.exports
to export values. Here is an example of a CommonJS module:
In the example above, we export the INTEREST_RATE
constant and three functions: deposit
, withdraw
, and getBalance
. The module.exports
is a JavaScript object. Any value assigned to module.exports
will be exported from the module.
These exported values can now be imported into other modules using the require
function:
The require
function is used to import modules in CommonJS. It takes a single argument, which is the path to the module you want to import. The exported values are stored in an object, and you can access them using dot notation. In this example, we destructure the object to get the deposit
, withdraw
, and getBalance
functions. Unlike import
statements in ES6 modules, require
statements can be conditional and placed anywhere in the code.
While CommonJS modules are still used in Node.js, ES6 modules have become the standard for JavaScript modules in the browser. ES6 modules are asynchronous, which allows for better performance and more flexibility in how modules are loaded. They also provide a more standardized way to import and export values between modules.
It is worth noting that CommonJS is not the only alternative to ES6 modules. There are other module systems like AMD (Asynchronous Module Definition) and UMD (Universal Module Definition) that have been used in the past. However, ES6 modules have become the de facto standard for JavaScript modules due to their simplicity, performance, and widespread adoption.
JavaScript modules are a powerful feature that allows you to organize and encapsulate related functionality and data. By using modules, you can create a more structured and maintainable codebase. ES6 modules provide a standardized way to import and export values between modules, making it easier to work with large codebases. Whether you are working in the browser or in Node.js, modules are an essential part of modern JavaScript development. By understanding how modules work and how to use them effectively, you can create more modular, reusable, and maintainable code.