12th Mar '25
/
1 comment

[Notes] JavaScript Modules

I recently began my journey to become a full-stack developer and am starting a series where I share my notes on what I’ve learned that day. This post on ES6 Modules is the first of many.

Note: This topic will not be clear w/o knowing JS concepts like variables, functions, classes and objects.

Intro

  • import and export keywords.
  • Modularity = each file is its own module, and constants, variables, functions, and classes defined in a file are private to that module unless they are explicitly exported.
  • Values exported in one module are available for use in modules that explicitly import them.
  • How modules are different from scripts: in regular scripts, top-level declarations of variables, functions, and classes go into a single global context shared by all scripts. With modules, each file has its own private context.
  • Code inside an ES6 module is automatically in strict mode. No need to add use strict directive at the top.
    • strict mode fixes language deficiencies, provides stronger error checking, increases security
    • in strict mode all variables must be declared

ES6 Exports

These symbols can be exported from a module: constant, variable, function, class.

export const PI = Math.PI;

export function degreesToRadians(d) { return d * PI / 180; }

export class Circle {
	constructor(r) { this.r = r; }
	area() { return PI * this.r * this.r; }
}

Instead of adding the export keyword before individual entities, we could specify them at the end like this:

export { Cirlce, degreesToRadians, PI };

The curly braces do NOT indicate destructuring of an object.

It is common for a module to export only one value (typically a function or a class):

export default class BitSet {
	// implementation omitted
}

Two types:

  • regular exports
  • default exports

Default exports with export default can export any expression incl. anon function expressions and anon class expressions. Curly braces with default exports indicate an object literal.

ES6 Imports

import BitSet from './bitset.js';

The default export of the specified module (bitset.js file in the same directory as the current module/file) becomes the value of the specified identifier in the current module.

When a module exports a single value as its default, you import it without curly braces. If it were a named export, you’d use curly braces.

The .js extension is often omitted in modern JavaScript environments from the module specifier.

The imported value is a const variable.

To import from an external package:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

'react-router-dom' is an external package, which is why it doesn’t need a relative path. This tells the module system to look for the package in node_modules.

To import values from a module that exports multiple values:

import { mean, stddev } from './stats.js';

For default exports that do not have a name, we provide a local name when importing.

For named or regular exports, we refer by those names when importing.

When importing from a module that defines many exports:

import * as stats from './stats.js';

An import statement like this creates an object and assigns it to a constant named stats. Each non-default export of the module being imported becomes a property of this stats object.

To invoke: stats.mean() and stats.stddev().

To include a no-exports module i.e., a module that does not export any value:

import './analytics.js';

The included module is run once.

Imports and Exports with Renaming

Use as keyword.

import { render as renderImage } from './imageutils.js';
import { render as renderUI } from './ui.js';

Re-Exports

A module can import symbols from other modules and export them.

In stats/stats.js:

import { mean } from './stats/mean.js';
import { stddev } from './stats/stddev.js';
export { mean, stddev };

The above can be combined into a single ‘re-export’ statement:

export { mean } from './stats/mean.js';
export { stddev } from './stats/stddev.js';

Suppose we wanted to re-export the mean() function but also define average() as another name for the function:

export { mean, mean as average } from './stats/mean.js';
export { stddev } from './stats/stddev.js';

If the functions in mean.js and stddev.js are not named exports and are instead default exports:

export { default as mean } from './stats/mean.js';
export { default as stddev } from './stats/stddev.js';

To re-export a named symbol from another module as the default export of your module:

export { mean as default } from './stats.js';

JS Modules on the web

If you want to natively use import directives in a web browser, you must tell the browser that your code is a module by using a <script type="module"> tag.

<script type="module">import './main.js';</script>

The inline JS code is called an inline module.

Scripts with type="module" attribute are loaded and executed like scripts with defer attribute. Loading of code begins as soon as the HTML parser encounters the <script> tag. In the case of modules, this code-loading step may be a recursive process that loads multiple JS files. But code execution does not begin until HTML parsing is complete. And once HTML parsing is complete, scripts (both modular and non) are executed in the order in which they appear in the HTML document.

If we want the inline module to be executed as soon as the code is loaded, add async attribute. This will not wait for HTML parsing to be completed.

A regular <script> tag will load a file of JS code from any server on the internet. But <script type="module"> code can only be loaded from the same origin as the containing HTML document or when proper CORS headers are in place to securely allow cross-origin loads.

Dynamic imports with import()

Static import:

import * as stats from './stats.js';

Not possible to use an expression that evaluates to a string literal for the module specifier, './stats.js'.

Dynamic import:

import('./stats.js').then(stats => {
	let average = stats.mean(data);
});

import('./stats.js') returns a Promise object that represents the asynchronous process of loading and running the specified module. When the dynamic import is complete, the Promise is fulfilled and produces an object – like the stats variable in the static import line earlier.

Or, in an async function:

async analyzeData(data) {
	let stats = await import('./stats.js');

	return {
		average: stats.mean(data);
		stddev: stats.stddev(data);
	};
}

With import(), the module specifier can be an expression that evaluates to a string.

import() is not a function invocation. It is an operator.

import.meta.url

import.meta object contains metadata about the currently executing module.

import.meta.url is the (absolute) URL from which the module was loaded.

Ex.:

function localStringsURL(locale) {
	return new URL(`l10n/${locale}.json`, import.meta.url);
}

When URL interface is instantiated with the new keyword, its constructor will be called and it takes 2 arguments. The first one is an absolute URL or a relative reference to a base URL (this is the case here). The second one is the base URL.

Get access to all 610 Bricks code tutorials with BricksLabs Pro

1 comment

  • Craig Davison

    Good article. things get a little interesting it would seem when using modules in WordPress development.

    I ran into an issue, where js files were inexplicably being cached and never updating after changes were made. So I implemented versioning of the files when enqueuing them WP this is fine however, when importing the JS is not inherently aware of the version of the file so the original un-versioned version of the file was being cached with the version e.g. my-script.js and my-script.js?ver=123456.

    my-script.js for some reason does not include any changes and am unable to remove it but it is there because the import references this and not my-script.js?ver=123456. What I ended up doing was pushing the version numbers out through the localised data object which I could then reference later building a dynamically generated url string:

    Here is an example: javascript const toastUrl = ./toasts.js?ver=${versions.toast}; // import {showToastNotification} from toastUrl; import(toastUrl) .then((module) => { window.showToastNotification = module.showToastNotification; }) .catch((error) => console.error("Failed to load toasts.js", error));

Leave your comment