Learn to Code via Tutorials on Repl.it!

← Back to all posts
The Modern JavaScript Tutorial Series; Part 2
xxpertHacker (649)

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)

Commentshotnewtop
xxpertHacker (649)

@19wintersp Oh yeah, I forgot to ask, did the tutorial help?

xxpertHacker (649)

@19wintersp Wait really? Imho, this was trash.

Also, you know a thing or two about promises, and I was thinking about making the next one about asynchronous control flow, so I'm thinking, threads, atomics, promises, events, concurrency, all of that, but in JavaScript.

There's a thread on HackTalk about part 3 here.

programmeruser (365)

CommonJS isn't horrible.
Also, there's also AMD:

define('myModule', function() {
  return {
    hello: function() {
      console.log('Hello World');
    }
  }
});
xxpertHacker (649)

@programmeruser

CommonJS isn't horrible

Fight me, don't even say that in strikethrough!

Also, there's also AMD

Oh yeah, I forgot about that one, I also know nothing about it, beyond the export syntax; I don't even know the import syntax.

Also, this "series" kinda died here (and it got drowned out by other tutorials), at part 2... any suggetestions/ideas for part 3?

programmeruser (365)

@xxpertHacker talk about Node.js. Or Deno which you seem to like better.

xxpertHacker (649)

@programmeruser Node.js's ecosystem and code has sadly evolved backward from how ES262 has evolved the language, so I tend to avoid it, but when I do use it, I forcefully try to use it like browser JS, which just creates a bunch of boilerplate code, converting callbacks to promises, trying to implement standard code, etc.

Now, Deno, no one seems to understand it, so I guess.
Yeah, that seems like a good idea, thank you.

realTronsi (840)

@xxpertHacker

It was actually harder to prevent global variable leakage than it was to prevent it

Ah yes it was harder to prevent it than to prevent it, makes sense.

Anyways decent tutorial, there wasn't as much content as I hoped there would be though

xxpertHacker (649)

@realTronsi Yup, made perfect sense.

And yes, there's not much to write about. I was trying to think about how much I could write about on modules, but there's not much to them.

The syntax is the most complicated thing that there could be, beyond that, it's pretty simple.

I think I had said before, that I doubted that I could write much about this, what else should I add?
(What should come next too?)

realTronsi (840)

@xxpertHacker hmm this is a modern js tutorial and modules are basically the core differences tbh, so I'm not really sure

xxpertHacker (649)

@realTronsi Should I amp it to "advanced tutorial"?

realTronsi (840)

up to you, but what topics do you have in mind? @xxpertHacker

xxpertHacker (649)

@realTronsi Nothing, still feel braindead right now when trying to explain advanced stuff.
I don't think of JS as a language where you can point out what someone did and say "whoa, that's advanced."

So I still don't know.

I have a plan, heading over to /ask right now, lmao.

realTronsi (840)

@xxpertHacker yeah, maybe some more advanced ways to use promises but that isn't special to modular js, and there really isn't much "advanced" stuff tbh

xxpertHacker (649)

@realTronsi Idk, I just feel like, if I see someone using C for some super-low level stuff, like interacting with an OS or kernel directly, I'd say "damn, that's pretty advanced," but then if I see someone using promises in JS, I'd think that I've been doing that for sometime, so it's nothing new.

Or if someone implemented complex stuff like the C++ random_device, I'd say it's advanced.

realTronsi (840)

@xxpertHacker well yeah, but also C isn't that advanced either if you think about it. C is very simple and logical so it's kind of like 2+2, while js is kind of like 1+1+1+1

I honestly don't know how to explain it tbh but if you know C the majority of the code you can understand, it's just a matter of logic and how well you utilize the code

xxpertHacker (649)

@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!?

realTronsi (840)

@xxpertHacker well for a lot of languages there's just complicated extra stuff. OS dev is very advanced, but the thing is the code isn't the thing that's advanced, it's logic and how they wrote it which is advanced

xxpertHacker (649)

@realTronsi So, technically, one could write an OS in a non-advanced way, without really thinking about how to make it work, and it wouldn't be advanced at all.

If so, then OS development itself isn't advanced, it's just that a lot of people who do it are advanced.

realTronsi (840)

@xxpertHacker no like, well technically true but a super simple example could be like someone nesting if statements to make a python rpg game vs those who actually use classes, functions, stuff like that.

So technically like for example writing code in binary isn't advanced, it's just 0s and 1s but the hard part comes from actually writing good logic and stuff

RohilPatel (1459)

Wow! Thanks for the plug....and the ping

RohilPatel (1459)

plug ==================== promo lol @xxpertHacker

xxpertHacker (649)

@RohilPatel I understand "promo" better, np, yw.

Hey, actually, wanna help me with part 3?
I'm lost as to what to write about, I'm looking for something advanced and complex.

Not an API, since they all have their own documentation.
Not syntax, because that is the most basic part of the language.
Any ideas?

It's okay if you don't want to join me, you do you, just tell me what your decision is. (alternatively, I'm bored and have nothing to do, wanna work on a project, or need help w/ anything?)

Also, while you're here, you would do well to read the post ^ and gimme a cycle, lol.

RohilPatel (1459)

well i mean clearcode 3rd gen is in development so u can help with tht if u want. its a lot better than u have ever seen it @xxpertHacker

xxpertHacker (649)

@RohilPatel I have my doubts. What do you want help with in the project?

What Repl type and language? Node.js?

RohilPatel (1459)

yeah nodejs...do u wanna be invited? @xxpertHacker

xxpertHacker (649)

@RohilPatel Ugh; okay, I'll join, but if I do, I need you to do one thing: layout a set of clear and explicit goals for the project.

I'll try to help make 'em happen.

RohilPatel (1459)

It would definitely be easiest if we all had discord. We do have a todo list btw lol @xxpertHacker