Categories
Modern JavaScript

Best of Modern JavaScript — Modules in Browsers and Import/Export Relationship

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at how to use JavaScript modules in browsers and the relationship between exports and imports.

ES6 Modules in Browsers

We can use ES6 modules in most modern browsers.

To use them, we just add a script tag with the type attribute set to module .

The file extension is still js like regular script files.

The content of the JavaScript file is delivered via the webserver.

ES6 modules in the browser combine to convenience by combining the syntax os synchronous loading with async loading underneath.

They’re less flexible than ES6 modules.

They can only be loaded at the top level so they can’t be conditional.

The restriction lets us analyze modules statically do see what modules are imported by other modules before execution.

Regular scripts can’t become modules because they’re synchronous.

We can’t import modules declaratively since they load one by one,

Therefore, a new type of script is added so we can load modules asynchronously.

To import a module in the browser, we can write:

<script type="module">
  import _ from "https://unpkg.com/lodash-es";

  console.log(_.uniq([1, 2, 2, 3]));
</script>

We import the ES6 module version of Lodash and used its methods.

The type attribute is set to module so we can import modules in our script.

Whether a JavaScript file is a module or a script is determined by where the code is written.

If we don’t have any exports or imports and use the file with a script tag without the type attribute set to module , then it’s a script.

Otherwise, it’s a module.

Imports’ Relationship with Exports

Imports’ relationship with exports different between module systems.

In CommonJS, imports are copies of exported value.

In ES6 modules, imports are read-only views of exports that updates as their values update.

For instance, if we have:

baz.js

module.exports = {
  add(x, y) {
    return x + y;
  },
  subtract(x, y) {
    return x - y;
  }
};

Then we can import it by writing:

const { add, subtract } = require("./baz");

const sum = add(1, 2);
const difference = subtract(1, 2);

A copy of the module is made when we require a CommonJS module.

This also applies when we import the whole module as one object.

So if we have:

const baz = require("./baz");

const sum = baz.add(1, 2);
const difference = baz.subtract(1, 2);

then baz is a copy of the baz.js module.

On the other hand, ES6 module imports and read-only views on the exported values.

Imports are connected live to the exported data.

If we import the whole module, then the whole imported module acts like a frozen object.

For example, if we have:

baz.js

export const add = (x, y) => {
  return x + y;
};

export const subtract = (x, y) => {
  return x - y;
};

foo.js

import { add, subtract } from "./baz";

const sum = add(1, 2);
const difference = subtract(1, 2);

add and subtract are read-only.

If we try to reassign an imported member to something, we’ll get an error.

We get the same result if we import the whole module:

import * as baz from "./baz";

const sum = baz.add(1, 2);
const difference = baz.subtract(1, 2);

We can’t assign a new value to baz .

Conclusion

ES6 module imports are read-only views on exports.

Also, we can use modules in the browser.

Categories
Modern JavaScript

Best of Modern JavaScript — Modules

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at how to use JavaScript modules.

Exporting Function Expressions

The export functions expressions, we can put parentheses around our export statements.

For example, we can write:

export default (function () {});
export default (class {});

Classes are functions in JavaScript so the same rules apply.

Default Exporting Values Directly

We can default export values directly.

For example, we can write:

export default 'foo';

to export the string 'foo' .

Also, we can write:

const foo = function() {};

export { foo as default };

We create a function foo and export it with the as default keywords to make default exports.

We need this syntax so that we can turn variables declarations into default exports.

Imports and Exports Must be at the Top Level

Imports and exports must be at the top level.

For instance, we can write:

import 'foo';

But we can’t write:

if (true) {
  import 'foo';
}

or

{
  import 'foo';
}

They’ll both raise SyntaxErrors.

Imports are Hoisted

Imports aren’t hoisted, so we can use them before they’re defined.

For example, we can write:

console.log(add(1, 2));

import { add } from "./math";

And the return value of add will be logged.

Imports and Exports

Imports are read-only.

This enables the module system to allow cyclic dependencies.

Also, we can split code into multiple modules and it’ll still work as long as we don’t change the value of them.

Cyclic Dependencies

Cyclic dependencies are where 2 modules import members from each other.

They should be avoided since it makes both modules tightly coupled.

However, we may not be able to eliminate them altogether, so we’ve to live with them.

We can add cyclic dependencies in ES6 by wiring something like the following

For instance, we can write:

math.js

import { foo } from "./index";

export const bar = () => {
  foo();
};

index.js

import { bar } from "./math";

export const foo = () => {
  bar();
};

We import foo from index.js in math.js and use the imported function.

Likewise, we import bar from math.js and call that.

Other Importing Styles

In addition to named and default exports.

We can use import to just load the module and don’t import anything.

For example, we can write:

import 'lib';

Also, to rename imports, we can use the as keyword.

For example, we can write:

import { name as foo, bar } from "baz";

The as keyword is used to rename a named export name .

We can also use it to rename a default export.

For example, we can write:

import { default as foo } from "baz";

We can also use the as keyword by writing:

import * as baz from "baz";

to import the whole module and name it as baz .

Default imports can be mixed with named imports.

For example, we can write:

import foo, { bar, qux } from "baz";

Conclusion

We can export and import module members in various ways,

Cyclic dependencies also work with ES6’s module system.

Categories
Modern JavaScript

Best of Modern JavaScript — Module Details

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at the design of the ES6 module system.

Use a Variable to Specify from which Module I want to Import

We can specify which module to import with the import function.

It takes a string with the path to the module.

For instance, we can write:

(async () => {
  const foo = await import("./foo");
  //...
})();

to import a module with the import function.

It takes a string so we can pass in a string generated dynamically.

It returns a promise so we use await to get the resolved value.

Import a Module Conditionally or On-demand

With the import function, we can import a function conditionally or on-demand.

For instance, we can write:

(async () => {
  if (Math.random() < 0.5) {
    const foo = await import("./foo");
    //...
  }
})();

to import a module conditionally.

Using Variables with Import Statements

We can’t use variables with our import statements.

So we can’t write something like:

import foo from 'bar-' + SUFFIX;

But with the import function, we can write:

(async () => {
  if (Math.random() < 0.5) {
    const foo = await import(`bar-${SUFFIX}`);
    //...
  }
})();

Use Destructuring in an import Statement

We can’t use nested destructuring in an import statement.

This makes sense because exports can only be done at the top level.

It looks like destructuring but the syntax is different.

Imports are static and views on exports.

So we can’t write something like:

import { foo: { bar } } from 'some_module';

Named Exports

With named exports, we can enforce a static structure with objects.

If we create a default export with an object, then we can’t statically analyze the object.

The object can have any properties and they can be nested.

eval() the Code of a Module

We can’t call eval on module code.

Modules are too high level for eval .

eval accepts scripts which doesn’t allow the import or export keywords.

Benefits of ES6 Modules

ES6 modules come with several benefits.

They include a more compact syntax.

Static module structure also helps with eliminating dead code, static checks, optimizations, etc.

Also, we check for cyclic dependencies.

With a standard module system, we eliminate the fragmentation of multiple module systems.

Everything using old module systems will migrate to ES6 standard modules.

Also, we don’t have to use objects as namespaces anymore.

This functionality is now provided by modules.

Objects like Math and JSON serve as namespaces for segregating entities.

Conclusion

ES6 modules provide us with many benefits over older non-standard module systems.

Also, they can be dynamically imported.

They allow for various optimizations.

Categories
Modern JavaScript

Best of Modern JavaScript — Module Design

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at the design of the ES6 module system.

ES6 Modules Design

ES6 modules are designed with specific properties in mind.

One of them is that default exports are favored.

The module structure is also static.

It supports both synchronous and async loading.

Cyclic dependencies between modules are also allowed.

Default exports are made to be as convenient as possible.

Also, modules are static so that they can be checked statically at compile time.

We only need to look at the code and don’t have to run it.

Therefore, we can’t write something like:

if (Math.random() < 0.5) {  
  import foo from 'foo';  
} else {    
  import bar from 'bar';  
}

with ES6 modules. But we can write something like:

let lib;  
if (Math.random() < 0.5) {  
  lib = require('foo');  
} else {  
  lib = require('bar');  
}

with CommonJS modules.

ES6 modules force us to import and export statically.

The benefit of static imports is that we can remove dead code when bundling.

The files we developed are usually bundled into one large before going into production.

After bundling, we need to load fewer files in order to load all the modules.

Compressing the bundled files is more efficient than bundling more files.

Also, unused exports can be removed during bundling to save space.

If the bundle is transferred over HTTP/1, then the cost of transferring multiple files is high.

But it doesn’t matter with HTTP/2 since there’s caching.

Having a standard module system eliminates the need for custom bundle formats.

The static structure of the means the bundle format doesn’t have to worry about conditionally loaded modules.

Read-only imports mean that we don’t have to copy exports.

Since they don’t change, we’ve to refer to them directly.

Imports being references to the original also means that lookup is faster.

CommonJS imports are whole objects, which are much bigger than references.

If we import a library in ES6, we know its contents and can optimize access.

Variable checking can also be done with a static module structure.

We know which variables are available at which location.

Global variables are no longer needed to be created to share resources and we would only be referencing global variables.

Module exports will be known immediately.

Variables that are local to modules will also be known.

The same checks can be done with other tools like linters like ESLint and JSHint.

Support for Both Synchronous and Asynchronous Loading

ES6 modules support both synchronous and async loading.

The import and export keywords allow for synchronous imports and exports.

There’s also the import function to let us import modules dynamically in an async manner.

The functions return a promise which lets us import a module.

Support for Cyclic Dependencies Between Modules

Cyclic dependency is a key goal of ES6 modules.

They can happen anywhere and they aren’t evil.

It can happen in large systems as we refactor our code.

Making ES6 modules support cyclic dependencies makes our lives easier since we don’t have to worry about them.

Conclusion

ES6 modules have multiple goals in mind.

Their design lets us statically analyze them and bundle them easily.

Cyclic imports are also supported.

Categories
Modern JavaScript

Best of Modern JavaScript — Module Basics

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at how to use JavaScript modules.

Before ES6

ES5 or earlier don’t have a native module system.

Therefore, there’re various module systems that were created to solve the problem of organizing code.

There’s the CommonHS module system that’s standard ib Node.js.

It has a compact syntax and loads modules synchronously and it’s usable on the server-side.

The Asynchronous Module Definition module system is another popular mobile system.

It has more complex syntax, which lets them work without eval or a compilation step.

ES6 Modules

ES6 modules create a formal module system that’s standard to JavaScript.

It has a compact syntax and lets us do single exports.

Also, it has support for cyclic dependencies.

There’s direct support for async loading and the loading is configurable.

The syntax is even more compact than the ES6 module syntax.

And it has support for cyclic dependencies.

This is better than CommonJS.

The standard module system has a declarative syntax for imports and exports.

And it has a programmatic loader API to configure how modules are loaded and to conditionally load modules.

Named Exports

With named exports, we can export more than one member of a module.

For instance, we can write:

math.js

export const sqrt = Math.sqrt;
export function add(x, y) {
  return x + y;
}
export function subtract(x, y) {
  return x - y;
}

to create a module that has several functions exported with the export keyword.

Then we can import the items by writing:

import { add, subtract } from "./math";

const sum = add(1, 2);
const difference = subtract(1, 2);

We imported the items from the math.js module.

The named exports are in the curly braces.

Then we can call the functions that we exported below it.

With CommonJS, we use the module.exports property to export multiple members of a module.

For instance, we can write:

math.js

const sqrt = Math.sqrt;
function add(x, y) {
  return x + y;
}
function subtract(x, y) {
  return x - y;
}

module.exports = {
  sqrt,
  add,
  subtract
};

index.js

const { add, subtract } = require("./math");

const sum = add(1, 2);
const difference = subtract(1, 2);

We call require to require the whole module, and then we destructured the entries from the imported module.

Then we can use the imported functions the same way.

Default Exports

Default exports are a type of export that can only happen once in any module.

We can name them anything when we import default exports.

For instance, we can write:

math.js

export default function add(x, y) {
  return x + y;
}

to export the add function as a default export.

And then we can import the function by writing:

index.js

import add from "./math";

const sum = add(1, 2);

To export a class, we can write:

Foo.js

export default class {}

We don’t need a semicolon after the statement.

Then we can import it with:

import Foo from "./Foo";

const foo = new Foo();

We can include or exclude the name for default exports.

So we can write:

`export` `default` `function` baz`() {}`
`export` `default` `class` `Bar` `{}`

or

`export` `default` `function() {}`
`export` `default` `class {}`

Conclusion

Before ES6, there’s no module system that’s standard to the language.

Since then, JavaScript has a native mobile system that we can use to organize our code in a standard way.