Modules
Modules: Modules the Basics
What is a JavaScript module?
View Answer:
// we have a file sayHi.js exporting a function:
// 📁 sayHi.js
export function sayHi(user) {
console.log(`Hello, ${user}!`);
}
// Then another file may import and use it:
import { sayHi } from './sayHi.js';
console.log(sayHi); // function...
sayHi('John'); // Hello, John!
Do JavaScript modules work with the standard local file:// protocol?
View Answer:
We can use a local web server, such as static-server, or use the live server capability of your editor, such as VS Code Live Server Extension, to test modules.
Why should you use modules in JavaScript?
View Answer:
How do you create a module in JavaScript?
View Answer:
Here's a simple example of a JavaScript module:
greetings.js (Our module file)
export function sayHello(name) {
return `Hello, ${name}!`;
}
main.js (File where we import and use the module)
import { sayHello } from './greetings.js';
console.log(sayHello('JavaScript')); // Outputs: Hello, JavaScript!
What is different in modules compared to regular scripts?
View Answer:
Each module has a separate top-level scope. Top-level variables, methods, and functions from a module, in general, are not visible in other scripts.
The import.meta object contains information about the current module. The surroundings determine its content. The browser includes the URL of the script or if it is inside HTML, the URL of the current webpage.
In top-level modules, this is undefined.
Module scripts always defer, same as the defer property for external and inline scripts.
For non-module scripts, the async attribute only works on external scripts. Async scripts run immediately when ready, independently of other scripts or the HTML document. For module scripts, it works on inline scripts as well.
Can you use undeclared variables in JavaScript modules?
View Answer:
<script type="module">
a = 5;
// results in a syntax error
</script>
What does module-level scope mean in JavaScript?
View Answer:
For instance, consider the following module...
// myModule.js
let privateVar = 'I am private';
export let publicVar = 'I am public';
function privateFunc() {
console.log('This is a private function');
}
export function publicFunc() {
console.log('This is a public function');
}
In this module, privateVar
and privateFunc
are only available within myModule.js
— they are in the module-level scope. On the other hand, publicVar
and publicFunc
are exported from the module, so they can be imported and used in other modules:
// anotherModule.js
import { publicVar, publicFunc } from './myModule.js';
console.log(publicVar); // I am public
publicFunc(); // This is a public function
Trying to access privateVar
or privateFunc
from anotherModule.js
would result in an error, because they are not exported from myModule.js
and are therefore not available outside of that module's scope.
This is a powerful feature that helps you write cleaner, more maintainable code by explicitly controlling what is and isn't exposed to the rest of your application.
Is a module imported into multiple places re-evaluated each time it gets imported?
View Answer:
// 📁 console.log.js
console.log("Module is evaluated!");
// Import the same module from different files
// 📁 1.js
import `./console.log.js`; // Module is evaluated!
// 📁 2.js
import `./console.log.js`; // (shows nothing)
What does the JavaScript import.meta object do?
View Answer:
Syntax: import.meta
<script type='module'>
// returns script url - url of the html page for an inline script
console.log(import.meta.url);
</script>
Is there anything specific that we should remember about the “this” keyword in JavaScript modules?
View Answer:
<script>
console.log(this); // window
</script>
<script type="module">
console.log(this); // undefined
</script>
How are module scripts loaded in the browser?
View Answer:
<script type="module">
console.log(typeof button); // object: the script can 'see' the button below // as
modules are deferred, the script runs after the whole page is loaded
</script>
<script>
console.log(typeof button); // button is undefined, the script can't see elements below
// regular scripts run immediately, before the rest of the page is processed
</script>
<button id="button">Button</button>
What is the difference between inline asynchronous scripts and a module?
View Answer:
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
import { counter } from './analytics.js';
counter.count();
</script>
Can you explain how external scripts with type="module" are different from scripts without it?
View Answer:
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
// r
<script type="module" src="my.js"></script>
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
If a module script gets fetched from another origin, the remote server must supply a header Access-Control-Allow-Origin allowing the fetch. That ensures better security by default.
In node.js bare modules are common, are bare modules allowed in browser imports?
View Answer:
import { sayHi } from 'sayHi'; // Error, "bare" module
// the module must have a path, e.g. './sayHi.js' or wherever the module is
Certain environments, such as Node.js or bundle tools, allow bare modules with no path since they have methods of identifying modules and hooks to fine-tune them. However, browsers do not currently allow bare modules.
What is the fallback script type for older browsers for JavaScript modules?
View Answer:
<script type="module">
console.log('Runs in modern browsers');
</script>
<script nomodule>
console.log('Modern browsers know both type=module and nomodule, so skip this');
console.log(
'Old browsers ignore script with unknown type=module, but execute this.'
);
</script>
What is a circular dependency in modules?
View Answer:
Here's an example to illustrate:
Module A:
import { foo } from './moduleB.js';
export function bar() {
return foo();
}
Module B:
import { bar } from './moduleA.js';
export function foo() {
return bar();
}
In this example, moduleA
imports moduleB
because it needs to use the foo
function. At the same time, moduleB
imports moduleA
because it needs the bar
function. This creates a circular dependency, where each module is waiting for the other one to finish loading before it can fully execute.
Circular dependencies can lead to problems such as:
1. Initialization issues: If module A depends on module B to initialize, and module B simultaneously depends on module A to initialize, neither can properly initialize because they are waiting for each other.
2. Maintenance issues: Circular dependencies can make the code harder to understand, reason about, and maintain, because it's not clear which module can be safely modified or removed without affecting the other.
3. Testing issues: Circular dependencies can make unit testing more difficult, because each module in the cycle may need the other modules to be present in order to be tested.
To resolve circular dependencies, you can refactor your code to remove the circularity. This may involve moving some code to a third module that both of the original modules can depend on, or rethinking your design to minimize dependencies between modules.
How can you handle circular dependencies?
View Answer:
1. Rethink your design: The best way to handle circular dependencies is to avoid them by refactoring your code. If two modules depend on each other, it might be because their responsibilities are not well-separated. Splitting responsibilities differently, or creating a third module that both can depend on, can often eliminate the circular dependency.
2. Dynamic Imports: Instead of static top-level imports, you could use dynamic imports at the time when the module function is called. This allows the modules to load completely before trying to call the function from the other module.
3. Partial Exports: JavaScript module system handles circular dependencies by doing a partial export. When you import something from a module that hasn't finished executing yet, you'll get a live reference to the export. It means you get whatever the current value of that export is at the time you access it, not when you first import it.
Here is an example of how JavaScript handles this:
Module A:
import { foo } from './moduleB.js';
export function bar() {
return foo();
}
console.log(foo()); // Prints "This is function foo"
Module B:
import { bar } from './moduleA.js';
export function foo() {
return "This is function foo";
}
console.log(bar()); // Prints "This is function foo"
In this case, both modules are loaded successfully without any errors. JavaScript ES6 modules handle circular dependencies by partially resolving the imported modules, and then updating them once the modules are fully evaluated.
Despite this, the best practice is to avoid creating circular dependencies whenever possible, as they can make code more difficult to understand and maintain.
What are the benefits of using ES modules over CommonJS modules?
View Answer:
1. Static Analysis: ES Modules are static, which means the structure of imports and exports are determined at compile time. This allows for tooling improvements, such as better static analysis, tree shaking (removal of unused exports for smaller bundle sizes), and faster lookups of import bindings. In contrast, CommonJS modules are dynamic and determined at runtime, which prevents these types of optimizations.
2. Async and Parallel Loading: ES Modules can be asynchronously and parallelly loaded, which can lead to performance benefits, especially in a browser context.
3. Native support in browsers: ES Modules are natively supported in modern browsers. This can make it easier to share code between the server (Node.js) and the client (browser), and allows you to use modules in the browser without needing a bundling tool like webpack or Browserify.
4. Named and default imports/exports: ES Modules support both named and default imports/exports, providing more flexibility in how you expose module functionality. CommonJS, on the other hand, only supports a single export object.
5. Better compatibility with future features: The static nature of ES Modules means they are better suited for compatibility with future ECMAScript features.
However, it's important to note that as of my last training data in September 2021, Node.js, which is used for server-side JavaScript, has more mature support for CommonJS. While ES Modules are supported in Node.js as of version 13 (with the --experimental-modules
flag in earlier versions), there may be compatibility issues with some npm packages which expect CommonJS.
As a result, many developers continue to use CommonJS for server-side Node.js code, or use a combination of both ES Modules and CommonJS, depending on the specific needs of their project. Always consider your specific use case and constraints when deciding between ES Modules and CommonJS.