My last post covered some of the general import syntax of modules and today I’m going to go deeper into some of the features that tend not to be covered when people talk about modules. Similar to my last post I’m going to try to stick to facts in the first part and super biased opinions in the second part.

Bindings and what not

In JavaScript numbers and strings are references to specific values and are passed around by reference, in other words if you do:

var a = 3; var b = a; a++;

a is now equal to 4 and b is equal to 3, because a and b were both just references to the number 3, when you incremented a all that did was set a equal to 4, it didn’t effect b as that was a separate reference to the number 3.

On the other hand objects (and thus arrays and functions) do not work that way so:

var a = {num:3} var b = a; a.num++;

now both a.num and b.num are both equal to 4. That is because a and b do not both refer to objects that have the value {num:3} but instead they refer to the same object that originally had a value of {num:3} .

In the ES6 module syntax if you want to export an object you can do it via

// lib.js export default { num: 3, increment: function () { this.num++; } }

If you then import it into multiple files they are just references to the same object so

// a.js import a from './lib.js' console.log(a.num); // 3 setTimeout(function (){ a.increment(); }, 1000);

and

// b.js import b from './lib.js' console.log(b.num); // 3 setTimeout(function (){ console.log(b.num); // 4 }, 2000);

The other thing to note is that if there was no increment method on the exported object, you would only get an error when you tried to use the method, a second into the script being run.

In ES6 you can also write the script as follows

// lib.js export let num = 3; export function increment () { num++; }

then

// a.js import {num, increment} from './lib.js' console.log(num); // 3 setTimeout(function (){ increment(); }, 1000);

and then

// b.js import {num as b} from './lib.js' console.log(b); // 3 setTimeout(function (){ console.log(b); // <-- what does this print }, 2000);

It prints 4, this is because what modules export are bindings. Put another way, you could think of ‘num’ and 'b’ as both being references to an object, but the object has a value of 3 (and latter 4) instead of it being a set of key value pairs.

Static Analysis

The other thing to note is that if there was no increment function exported from lib.js then instead of getting an error 2 seconds into running module a.js you would get an error before a.js was run due to modules being statically analyzed before they are evaluated and then imports fetched before hand, in other words somewhat how browserify works (browserify actually works more like the first example as it would only throw a static error if you had an import for a module that didn’t exist, but this is contrasted to node.js and unbundled AMD modules which only bother to look up whether a module exists when the code that imports it is run).

Circular dependencies

A tricky thing in any module system is how to deal with situations where 2 modules require each other. After some discussion with Guy Bedford I somewhat understand how ES6 deals with them. Basically function declerations hoist accross modules so this example (from Guy’s es6-module-loader) would work:

even.js

import { odd } from './odd' export var counter = 0; export function even(n) { counter++; return n == 0 || odd(n - 1); }

odd.js

import { even } from './even'; export function odd(n) { return n != 0 && even(n - 1); }

but on the other hand

a.js;

export var obj = {}; import {something} from './b'; something();

b.js

import {obj} from './a'; obj.val = 'asdf'; export function something() {}

would throw an error because 'obj’ wouldn’t be a object yet when b runs because … I’m just going to quote Guy here

The point here is that obj is bound by the time b executes, but the assignment has not been made as we still have to execute sources in some underlying order.

In other words you can think of them as being like regular JavaScript variables where the variable to be used is initialized at the top of the scope, but doesn’t have a value until later on.

Commentary

Static Analysis

Lets look at the static analysis first, this has the benefit of preventing a whole class of errors from ever happening in the first place, namely importing something that doesn’t exist but that you don’t use until later. For example in node you could have a file that has

//lib.js exports.foo = function () {/*someting*/};

and then another with a typo

//app.js var lib = require('./lib'); function rarelyRun(){ lib.fooo(); }

You would have to actually run the rarelyRun function to spot that error in node.js, but in ES6 modules you this would cause an error the first time you tried to run it.

The downside of static analysis isn’t actually anything to do with the static analysis but what is done in the name of static analysis, namely you can’t put import (or export) statements anywhere except top level code, i.e. no

var bar, baz; if (foo) { import {_bar, _baz} from 'bat'; bar = _bar; baz = _baz; } else { bar = 'bar'; baz = 'baz'; }

Putting statically analyzed imports inside conditionals is less stupid that it seems at first glance as it makes writing polyglot programs that can run in different environments possible. For instance, it’s common to see something like the following in the wild:

var myThing = something; if (typeof define === 'function' && define.amd) { define(function () { return myThing; }); } else if (typeof exports === 'object') { module.exports = myThing; } else { window.myThing = myThing; }

this exports your module as an AMD one if that exists, a Common JS one if that exists and otherwise assumes you are in a regular browser and attaches it to the window. Creating things like this is impossible in ES6 modules.

Circular References

This is a step back compared to Node.js, remember this

a.js;

export var obj = {}; import {something} from './b'; something();

b.js

import {obj} from './a'; obj.val = 'asdf'; export function something() {}

well the node.js version of this

a.js

exports.obj = {}; var b = require(./'b'); b.something();

b.js

var a = require('a'); a.obj.val = 'asdf'; exports.something = function() {};

would work fine. Why this works is best stated in the require.js documentations

[…] use exports to create an empty object for the module that is available immediately for reference by other modules. By doing this on both sides of a circular dependency, you can then safely hold on to the the other module.

The ES6 version seems like a step back compared to CJS/AMD as it doesn’t cover as many of the edge cases.

Mutable Binding

The ability to import a name, from a module, and have it look like a global variable while at the same time changing like an object property is the killer ES6 module feature nobody asked for, nobody needs, and nobody wants.