When I began as a web developer, one of the first projects I worked on had multiple JavaScript files all loaded into the browser using script tags. This was a fragile method for managing multiple files. With one file having a dependency on another, the ordering of these script tags were critical. One script tag out of order would crash the application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Script Tag Example</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
<script src="./scripts/main.js"></script>
</body>
</html>
In the example above, since jquery.js is a dependency of main.js, failure to load JQuery will throw an error! Or if the ordering of the script tags were reversed, main.js would try and use JQuery before it had been loaded into the browser. In a simple example like this it doesn't seem too difficult to manage, but with a large production application with multiple developers, it was a nightmare. When trying to refactor and clean up old script tags, a mistake could crash the app.
Another problem was that everything was loaded onto the global scope. It wasn't very hard to overwrite a variable by accident. Declaring a new variable with the same name of an already existing variable and the first variable was replaced(This was before ES6 introduced "const"). This would lead to bugs and unexpected behavior.
JavaScript started as a simple scripting language for the browser, mostly used for form validation and adding a bit of interactivity to otherwise static html pages.
In time developers began using the language to build larger and more dynamic applications. The codebase being used for web applications was growing. Splitting code between files and using third party libraries was a headache. The JavaScript ecosystem needed a module system.
The JavaScript community began to experiment and design their own module systems. CommonJS and Asynchronous Module Definition were two early specifications. For a time I was most familiar with Asynchronous Module Definition, or AMD for short. Working with ESRI's JavaScript API, which was built on the Dojo Toolkit gave me plenty of experience with the AMD pattern.
But in 2015 the JavaScript specifications finally included a built in module system. JavaScript Modules or ES6 Modules, while not immediately implemented, is today the standard in all browsers and NodeJS.
JavaScript Modules uses import and export statements to share code between files.
There are multiple ways to export code.
Named Exports
This is called a named export and will export the add function.
Using the below syntax, the function can be imported and used within another file.
You also have the ability to export multiple named exports, like in the snippet below
correspondingly, you would import these like this.
Another useful trick is to rename a named export when importing it.
This will allow two named exports with the same name, to be used in the same file and avoid variable collision. This is most common when using multiple third party code, where you don't have control over naming.
Default Exports
Another way to export is using a default export.
As a sidenote, a default export can be named within the exporting file and it will work the same. The name you give it will have no effect when importing it. The below snippet would work just the same.
const pi = 3.14;
export default pi;
Naming the default export pie will work the same as pi. It really doesn't matter how you alias a default import, as long as it isn't a JavaScript reserved word.
import pie from "./constants.js";
const radius = 3;
const radiusSquared = radius * radius;
console.log(pie * radiusSquared);
A file can have only one default export by definition and when importing it, it can be aliased with any name.
Another way that you will often see default exports used is with Object Property Shorthand. In the default export statement, an object literal will be created with all of the variables as the objects properties. The imported variable will be an object that contains all the exported functionality as the object's properties.
And of course you can mix named and default exports.
Module Object
One last module pattern I will mention is creating a module object with the import. By using the syntax import * as [object variable] from [source] you can wrap all of the imported functionality into a convenient object in the consuming code. '[object variable]' can be replaced with a variable name the contain the imported object. '[source]' can be replaced with the file location or the module identifier to indicate where the code lives.
Take a look at the refactor below for an example. Now the consuming code is importing the functionality into an object being aliased as 'calculator', and has the methods 'add' and 'subtract' bundled into it. A nice way to keep your code a bit cleaner.
In the next article I will go into more details about module identifiers.
Top comments (0)