Let's talk about eval
The mere presence of eval in a
function thwarts almost any kind of optimization or static
analysis.
Most JavaScript programmers I know
consider eval more harmful
than goto.
Heaven forbid a snippet of user input should end up in one of
those strings!
There's no good way for transpilers to
support eval, so they
basically don't even try.
As a community we have decided
it is never acceptable to
use eval
when there is any other way
to solve the problem.
And yet we
use eval all the time.
Of course it hides behind many different abstractions:
-
<script src="http://..."></script>
-
<script>...</script>
-
new Function("...")()
-
require("vm").runInThisContext("...")
-
<a href="javascript:...">Click me!</a>
-
<input type="button" value="No, me!"
onclick="..." />
-
setTimeout("...", 1000)
Nothing happens
unless first
an eval.
Carl Sandburg
There is an eval
in everything.
That's how the code gets in.
Leonard Cohen
There's no escaping eval
But it can be tamed, and Node does this
better than any other JavaScript platform,
thanks to
CommonJS.
CommonJS is great
-
All your code runs in a specific module scope.
-
Any given module runs at most
once, the first time
you require it.
-
Module source code is loaded from static files, according to
simple
rules.
-
Module load order emerges naturally.
-
Module exports remain distinct.
-
Global scope stays clean.
Once your code starts
running, you get to make all
the decisions about what additional code is allowed to run,
and when.
Has CommonJS won?
From the perspective of Node and NPM,it certainly seems so.
But what about code running in browsers?
Is "winning" really all we care about?
Can we do better?
What's wrong with CommonJS?
Problem 1: How code is loaded
Node has it easy. Just ask the file
system!
Not so simple or efficient to do hundreds of synchronous HTTP
requests over the network.
Instead, there has to be a way to deliver bundles of
code to the client.*
* Bundling woes deserve a talk of their own.Listen to
Trek Glowacki.
Problem 2: Dependency cycles
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
Problem 2: Dependency cycles
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What's wrong here?
Problem 2: Dependency cycles
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Why does this work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What about now?
Problem 2: Dependency cycles
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What happens when the first request comes in?
Problem 2: Dependency cycles
-
Not forbidden!
-
Relatively simple, deterministic
resolution policy.
-
Highly sensitive to load order, which
is beyond the control of the modules participating in the
cycle.
-
Example from the React codebase where
we gave up on modularity in the name of preventing cycles.
-
Cycles can be useful.
-
Cycles should work.
Problem 3:
exports vs.
module.exports
Immediately returning a partially populated
exports object in case of
circular dependencies would work so much better if
that exports object was
guaranteed to become complete eventually.
But modules can change the very identity of
exports by reassigning
the module.exports property,
rendering that partial
exports object totally
irrelevant.
Different modules that require the same module
can end up with totally unrelated results!
Problem 4: Multiple exports
It's definitely nice
that exports objects can have
multiple properties.
But it's nearly impossible to determine whether a certain
property is actually used.
Not a huge problem on the server, but client bundlers like
Browserify and
Webpack end up
including tons of dead code.
Self-discipline?
-
Never assign to
module.exports,
unless you are certain your module
has no (circular) dependencies.
-
Store references to imported exports objects:
var a = require("a"); // Mostly safe.
var foo = require("a").foo; // Dangerous!
exports.good = function (arg) {
return a.foo("good", arg); // Uses the latest value of a.foo.
};
exports.bad = function (arg) {
return foo("bad", arg); // Uses a stale value.
};
There's nothing about CommonJS that helps you (or your
teammates) keep this discipline.
CommonJS may be the most popular module system we have
But it's a shame that we're even talking about one module
system winning a popularity contest.
Almost every other language avoids this contest entirely, by
providing a built-in module system, and most
of them do it with a special syntax.
A native module system...
-
eases cooperation between authors and consumers of libraries,
-
eliminates the need for each library to provide its own
module-loading system,
-
regularizes the structure of applications, and
-
allows the developer community to stop debating the merits of different
code sharing mechanisms.
-
<blink> We have more interesting problems to solve!</blink>
A language without a native module system is a language in
which no one is ever quite sure how to share their code with the
widest possible audience.
Betting on the wrong best practice (CommonJS? AMD? UMD?) is a
recipe for obscurity, while reverting to the simplest common
denominator (global variables) feels like giving up.
And that's why the new ECMAScript 2015
import
and export statements are so
vitally important.
A native module system would be a huge
relief even if it was more restrictive
than CommonJS!
The Go module system simply forbids circular dependencies.
That's one solution.
If you don't like it, don't use Go.
But in fact the ES2015 module system was designed with all the
strengths and weaknesses of CommonJS in mind
Thanks to the hard work and cleverness of people like
Dave Herman,
ES2015 modules solve or at least
mitigate all four of the problems I mentioned earlier.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
import {readFile, writeFile} from "fs";
What's the difference?
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
import {readFile, writeFile} from "fs";
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Consider this usage example.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
What happens with the destructuring version?
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
let readFile = _fs.readFile, writeFile = _fs.writeFile;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Imported properties are simply stored in variables.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
let readFile = _fs.readFile, writeFile = _fs.writeFile;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
This has all the problems as our earlier example!
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
// Remember this hazard?
let foo = require("a").foo;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
This has all the problems as our earlier example!
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
import {readFile, writeFile} from "fs";
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
How about the ES2015 version?
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Behind the scenes, a reference to the module is imported.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
References to the imports then get "rewritten" as if they were
member expressions.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
With ES2015, ensureTrailingNewline
always uses the latest version of readFile
and writeFile.
So how does it work?
At first glance, the
ES2015 import statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Popular
package where this matters:
graceful-fs
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function a() {...}
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function a() {...}
// In d.js:
import a from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
// In d.js:
import a from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
// In d.js:
import a from "./abc";
import a1 from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
// In d.js:
import a from "./abc";
import a1 from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
import {default as a3, b, c} from "./abc";
What about module.exports?
ES2015 modules can define
a default exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
import {default as a3, b, c} from "./abc";
import a4, {b, c} from "./abc";
Default and named exports, together at last!
You might even say the ES2015 module
system enforces the discipline I outlined
earlier... which means it doesn't
have to be self-discipline.
And that makes all the difference.
Multiple exports?
ES2015 export statements must
appear only at the top level of a
module, and must have one of the
following forms—all of
which have names:
-
export var a = ...;
-
export let b = ...;
-
export const c = ...;
-
export function d() {...}
-
export function* e() {...}
-
export class F {...}
-
export default expression;
-
export {a, b, c, d, e as genFn, F};
Multiple exports?
ES2015 import statements
provide no way to obtain the exports object
itself.
In fact, native implementations of ES2015 modules need not
use objects to represent exports at all!
That's just an implementation detail that happens to be
convenient if you're compiling for an environment that
supports CommonJS.
This makes it possible to determine, statically,
for any form of import
statement, exactly which exports it does and doesn't care
about.
But enough about the future.
How can you use ES2015 modules today?
I would love to tell you, "Just use
Meteor!" And that will soon be the
case.
Until Meteor 1.3 is released, I've put together
a skeleton NPM package that you can clone
and modify, or just use as inspiration:
git clone https://github.com/benjamn/jsnext-skeleton.git
cd jsnext-skeleton
npm install
npm test
Try it, break it, republish it as your own, submit issues!
How can you use ES2015 modules today?
Some stuff you can do with this skeleton package:
-
write ES2015 in the src/ directory,
-
transpile from src/ into lib/
using an npm prepublish command,
-
run mocha
tests against code from both src/
and lib/, ensuring identical output, and
-
publish both lib/ and src/ code to
NPM, so everything Just Works™ but also ES2015-aware
tools like Rollup can work their magic.
Soon you'll be publishing both ES2015 and CommonJS to NPM,
but what use are either of those formats
to folks who really just want a .js file they can
load with a
<script> tag on a web
page?
To bundle, or not to bundle?
Some say: bundles need
to be created at publish time, so that library authors can deal with
bundling errors, instead of burdening the consumer.
Others say: if library
authors bundle in their dependencies, consumers of multiple
libraries may end up with conflicting copies of those
dependencies.
Still others: if we
could all just use the same language for writing modules, then
our bundling tools would have a much easier job.
The part of the talk where I sing the praises of Meteor
Meteor's build system takes care of pretty much every aspect
of bundling for you. Only binary
dependencies need to be published for multiple
architectures.
Meteor uses an optimizing
constraint solver (compiled from C++ to JS
using Emscripten)
to ensure compatible package versions.
Meteor will support ES2015 modules in version
1.3, if I have anything to do with
it. And I have everything to do
with it. No, seriously, you know
who to blame if modules don't make it into the 1.3 release.
My pleas:
-
Start writing ES2015 modules sooner rather than later,
assuming I have persuaded you that they are awesome.
-
No need for funny file
extensions. ES2015 is
.js.
-
Publish your source files to NPM, in addition to your
CommonJS files, so that sophisticated tools can begin making
use of them.
-
Add a
jsnext:main property to
your package.json, so the tools know where
to look.
The Importance of import
and export
Ben Newman
(Meteor)
EmpireNode 2015
{ github,
twitter,
instagram,
facebook
}.com/benjamn