The Modern JavaScript Tutorial Series; Part 2
xxpertHacker (768)

The Modern JavaScript Tutorial

Part 2: Modularization and Modernization


Part one is here.


To the haters out there like @Coder100 who suggest bundlers instead: all major browsers support modules,
https://caniuse.com/es6-module


Warning:
This tutorial overdoses on metasyntaical variables.


Modules

As JavaScript was intended to be a simple scripting language, easily able to be taught to amateurs, it rarely threw any errors, but instead had simply let operations silently fail, without notifying the code's author, ultimately making code hard to debug.

Eventually, it caught on that this was not ideal, yet, the language couldn't be changed to start throwing errors and acting more sensible, because the entire web would suddenly break. So, a backward-compatible solution was needed.

There is no way to explicitly mark a file with what version of JavaScript is being run; no such notion exists in JavaScript, as the web is supposed to be stateless.

The solution was a textual directive, known as the "use strict" directive, that would modify execution in newer browsers, without preventing execution in older browsers.
In newer browsers that implemented this directive, the runtime would only allow a subset of the language to be executed, and many operations would throw errors, allowing code authors to catch points at which they had done something incorrectly.

The directive manifested in source code as a literal string, appearing either inside a function or at the global scope. As it is not an executable statement, older browsers just skipped the string.

"use strict";

console.log("This should be in strict mode");

// example of checking for "strict mode" execution context
const isStrict = function() {
    return this === undefined;
}();

if (isStrict) {
    console.info("This environment is executing in strict mode!");
} else {
    console.warn("This environment is not executing in strict mode!");
}

But that was years ago, since then, another change had been made, making the directive redundant, as it effectively became the default.

Recall that JavaScript is a very unstructured, simple language.

If someone wrote a library in JavaScript, it would simply be loaded via an (X)HTML <script /> element, like so:

<script type="text/javascript" src="https://example.com/lib.js"></script>
<script type="text/javascript" src="./main.js"></script>

but, the type of "text/javascript" is already the default, so it could be left as the following:

<script src="https://example.com/lib.js"></script>
<script src="./main.js"></script>

All global variables declared in "lib.js" would "leak" into "main.js," allowing access to all variables.

It was actually harder to prevent global variable leakage than it was to allow it, which goes to show what type of thinking was put into the development of JavaScript.

Global variables are strongly discouraged nowadays, but checking for them was a common task.
One might have written code using the typeof operator at the global scope, as checking for non-existant variables did not throw an error when using the operator.

if (typeof foo === "undefined") {
    bar(foo);
    ...
}

Eventually, this had changed. A completely backward-incompatible change was finally introduced.

<script type="module" src="./main.mjs"></script>

The type was changed from the JavaScript MIME to "module." This meant that old browsers wouldn't even fire an HTTP request for the code, because they couldn't execute it properly anyway.

The common file extension for modular JavaScript is ".mjs".

"use strict" is the default execution mode in a module.

Everything is scoped to the file, global variables don't leak.

And, last but not least, two new keywords are now available in a module, export, and import.

Importing and exporting is the primary (and only) way to pass data between files.

Remember "lib.js" from earlier?

Now, instead of "leaking" global variables into your code, you would only use one script tag:

<script type="module" src="./main.mjs"></script>

And you would import the library's functionality from within the JavaScript, like so:

import { foo } from "https://example.com/lib.mjs";

console.log(foo(5, 3));

This is arguably a much more structured mechanism and a major win for JavaScript.
No longer do you have to add and remove script tags, you can do everything, from within the JS file.
If you are working a team developing a web page, now everyone can stick to their part of the code, as CSS has already had imports for some time, it was only time for JavaScript to get the same functionality too.

Yet, although this is such an advantage over the prior methods, there is one major roadblock that still makes many developers dislike it.

The biggest threat to modules is an entire JavaScript runtime that never implemented it: Node.js.

Many JavaScript users don't develop for the client-side web at all, but instead for the server-side!
Because Node.js has it's own ecosystem that evolved in an entirely different direction from the ES standards, there is a major divide between how code is written in and outside of the Node.js runtime.

Because many aren't used to modules inside Node.js, they either don't know about it at all, or they choose not to use it because it's not familiar to them.
But, there's an experimental flag allowing it to be used even there, many just don't use it.


Export

Exports are values that may be used by other modules.

The export keyword can be used behind a variable, when used, it allows the variable to be imported from another module.

A module has two slightly different types of exports, a default export, and a "normal" export.

A default export does not need to be a variable, it can be a value.

The syntax for exporting a default value:

export default "foo";

Then there are "plain" exports.
Normal exports must be bound to a variable within the source module.

There are a few different ways to use the export keyword.

One can export the value at the same time as they define it:

export const foo = ...;

Alternatively, one can export a variable later:

const foo = ...;
// ...
export foo;

Exports don't need to have the same name as the variable they are associated with:

const foo = ...;
export { foo as bar };

That same syntax can also be used to create a default export:

export { foo as default };

A large library might setup a whole directory of modules and export them all through one file, making it much easier to import.
One can re-export everything from another file, transparently passing them though, like so:

export * from "./module1.mjs";

The imports can be grouped into a Module object by giving it a name:

export * as module from "./module1.mjs";

The exceptions to the generic exporting syntaxes' are functions and classes:

export function foo() { ... };

export class T { ... };

The class or function must have a name, exporting an anonymous class or function results in a syntax error.


import

The import keyword allows other modules to gain access to the variables that are exported by other modules.

There are almost as many different ways to import a value, as there are ways to export one.

All forms of importing use the from keyword after it, proceeded by a URL pointing to a valid module.

To import a default value, you simply provide a variable name immediately after the import keyword.

import constant from "./module2.mjs";

To import anything else, put it within brackets after the default import.
And make sure that there is a comma after the default import.

import constant, { foo, bar, baz } from "./module2.mjs";

Just like exporting a variable, you may rename the import before using it:

import constant, { foo as quux, bar, baz } from "./module2.mjs";

To group all of the exports into one Module object, you can use the wildcard * symbol, followed by the as keyword:

import constant, * as vars from "./module2.mjs";

And, in all cases, the default import is optional; mix and match as you please.

Dynamic import

Almost forgot to include this section!

One can dynamically import a module, with a URL that is provided at runtime, by putting parenthesis after the import keyword.

This sort of pseudo-function will return a promise that resolves with all of the module's exports, with the default export quite literally called default.

Taking the last static import example, now using a dynamic import:

const { default: constant, ...vars } = await import("./module2.mjs");

And there is one more use for modules: arbitrary code execution.

import "./setup.mjs";
// or
import("./setup.mjs");

And that's all for importing.


Misc

While in a module, you gain access to some new values through the import.meta object!

One of the common values provided is the URL of the module that you are in.

// in ./main.mjs
const { url } = import.meta; // "https://example.com/main.mjs"

Yup, that's all there is to it, it's just a set of context-specific values.

You may be able to use them for feature detection too, as some implementations are already providing extra values here.


If no values from an import are used, then the import will not be fetched at all!

// mod.mjs

export const hello = "Hello, world!";
console.log(hello);
// main.mjs
import { hello } from "./mod.mjs";

// no log; an empty console awaits you

Static imports must be string literals!

Template strings will fail, string concatenation will fail, etc.

Start and end the string with ' or "; if you need a runtime string, then use a dynamic import.

Exports and static imports must also be done at the top-level scope, that is, you may not put them within brackets, if statements, functions, etc.


All importing is done asynchronously, and oftentimes in parallel.

Do not depend on any specific execution order.

(If I'm wrong, please correct me)


All modules are cached; two modules importing the same value will receive the same value.


All imports require a location specifier, such as "./", "../", etc.

This will fail!

import * as lib from "lib.mjs";

When updating a mutable variable that is exported, ex:

export let mutableVar = 0;

export const mutateVar = () => {
    ++mutableVar;
};

the change will be reflected across all modules that have imported it.

Imports cannot be mutated by an importing module, even if it was declared with the let keyword, because they are not considered to be variables, they are imports.

Imports do not create constant bindings to a value, const does that.

Similar to const, they do not create immutable bindings; you may still modify an object's properties, ex:

import object from "./module";

object.a = NaN;

Conclusion to modules

The JavaScript module system provides a powerful, robust means of managing much larger projects than ever before.
Like everything else that JavaScript provides, the module system is a tool to be used, not to be feared.
Learn to use it, put it to use, and wield it well.

Modularizing code allows reusability, and plenty of developers, such as myself, are reaping the rewards, why aren't you!?


If you want to learn about Node.js' non-standard, garbage module system, you can check out @RohilPatel's clickbait Node.js module tutorial here on Repl.


If you have questions about when you should use each type of import/export, leave a comment on this post.
(I won't answer you, because I suck at explaining stuff)

You are viewing a single comment. View All
xxpertHacker (768)

@realTronsi But that's with literally everything and every language though.
Usually, I'd say that OS dev isn't even that advanced either.

So, is nothing advanced!?