• This repository has been archived on 09/Oct/2019
  • Stars
    star
    192
  • Rank 202,019 (Top 4 %)
  • Language
    JavaScript
  • Created over 11 years ago
  • Updated about 10 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

DEPRECATED: Transpile non-ES6 let-blocks into ES6 (or ES3)

let-er

ES6 is bringing the let x = "foo" syntax to JavaScript, which basically hijacks any block and scopes your declaration to that block (instead of hoisting to the containing function as in ES5 and below).

Two problems, however:

  1. ES6 is a long time from being fully ubiquitous.
  2. And even when ES6 arrives, the more preferable let ( .. ) { .. } block syntax was rejected and so won't be valid. This is most unfortunate, since that syntax has a more readable form, creating explicit blocks for block-scoping instead of hijacking them.

let-er solves both these problems. By simply writing let ( .. ) { .. } style code, and then running your code through let-er as a build-step, your block-scoping will work exactly as you expect, today.

By default, let-er transpiles your let ( .. ) { .. } blocks into standard ES6 { let .. ; .. } blocks. If you pass the es3 option setting, you can even target ES3+ browsers, using the try..catch hack.

What does it look like?

Write block-scoped code like this:

let (x = "foo") {
	console.log(x); // "foo"
}

console.log(x); // Reference Error!

NOTE: let-er does not touch let ..; declaration syntax (similar to var ..;), only the (more preferable and more readable) let ( .. ) { .. } statement syntax. If you use let ..; declarations, let-er will simply skip over them.

Why block-scoping anyway?

You may be wondering what's the big deal with block-scoping? What difference does it make?

In my "For and against let" blog post, I lay out the case in detail. But let's briefly break the question down into two parts:

  1. How does block-scoping affect my coding style/maintainability/behavior?
  2. How does block-scoping affect my code performance (memory, GC, etc)?

Coding Style/Maintainability/Behavior

There's a concept in computer science called the "Principle of Least Exposure", which is broadly applicable in a lot of areas, but in particular here, we are using that to mean: "expose a variable and its value for only as long as it's needed, no more, no less."

Take this code snippet:

function foo() {
	var i, ret = [];

	// 100+ LoC doing other stuff with `ret` only

	for (i=0; i<ret.length; i++) {
		// ... other stuff with `i` and `ret`
	}
}

The fundamental question is, is it a good thing to have defined i as a "global" (to the scope of function foo() of course) variable, or would it have been better to keep it around (aka, "expose it") only for the loop that uses i?

What if we had done:

function foo() {
	var ret = [];

	// 100+ LoC doing other stuff with `ret` only

	for (var i=0; i<ret.length; i++) {
		// ... other stuff with `i` and `ret`
	}
}

That keeps i only referenced in the place he's used. That's an improvement.

And yet, because of declaration hoisting, that snippet will behave exactly the same as the previous snippet. The engine sees no difference.

Another example:

function foo() {
	var ret = [];

	// 100+ LoC doing other stuff with `ret` only

	if (ret.length > 10) {
		var i = ret.length;
		// do other stuff with `i` now, but only inside this `if`
	}
}

See how we've made it clear from a coding style perspective that i is only for that part of the function?

By contrast, ret is a variable we use throughout the function foo(), so it's perfectly fine and suitable to function-scope it.

Unfortunately, we've created a little bit of cognitive dissonance here between the code style of where i is declared, and what the actual scoping of i is, because i will be hoisted to the whole function regardless of our code style.

That means if you accidentally try to reference a variable i elsewhere in the code, maybe as a typo, you'd wish that you got some sort of helpful error like a "ReferenceError" that points out your mistake, but you won't get that, because i in fact exists everywhere inside of foo() even if it has no meaningful value yet.

Herein we see a code maintainability (aka "style") benefit of the new ES6 let keyword for block-scoping. By doing this:

function foo() {
	var ret = [];

	// 100+ LoC doing other stuff with `ret` only

	if (ret.length > 10) {
		let i = ret.length;		// <===== Notice the `let` !!?!?!!?!
		// do other stuff with `i` now, but only inside this `if`
	}
}

Now, the behavior of i will be to be block-scoped to that part of the code, and it will be clear not only stylistically but also functionally that i doesn't exist anywhere but in that block. You'll get helpful "ReferenceError" errors if you try to reference i elsewhere. Yay.

Code Performance

Now, let's examine how let may affect your code's performance. I say "may" for a couple of reasons. First, the number of engine implementations of let is pretty limited, so we can't really test in the real world just how much it will or won't affect performance. We can make intelligent guesses about the possibilities.

Secondly, what the engine can do in theory and what it will do in the real-world are often very different. These are implementation details and we should be careful not to get too much "into the weeds" here. Only those who actually make the engines are qualified to really obsess too much here.

But let's reason a little bit.

Consider:

function foo() {
	// 1000+ LoC of a bunch of complex stuff
}

function bar() {
	// a small simple function
	// note: does not use `foo()` at all
}

setTimeout(bar,60000); // make bar's closure stay around awhile

for (var i=0; i<10; i++) {
	foo(); // only places `foo()` is used
}

The bar() function has a closure over everything we see here in this scope, including the foo() function it doesn't use at all. Turns out foo() is a big hairy complex function so its memory footprint is not "zero".

For the 60 seconds that bar() is kept alive, his closure over this whole scope is also kept alive, meaning foo() is kept alive. But does foo() really need to be kept alive? Depends.

If we (and by "we", I mean the engine, of course!) know for sure that bar() never needs foo(), we might be able to make an intelligent implementation optimization that culls foo() out of the closure that bar() keeps around. We might, we might not. If bar() has anything inside him that we can't lexically analyze, like a with or eval, all bets are off the table. Who knows if bar() needs foo() or not? Better safe than sorry. Also, who knows if the engine actually does smart closures or not?

In theory they might. But in practice, the safer bet is to assume they don't or can't, and code a little defensively.

Here's where let block-scoping can help. It acts as a very clear signal to the engine what we intend for the lifetime of foo(). Therefore, the engine doesn't have to guess nearly as much. It can tell for sure. Consider:

function bar() {
	// a small simple function
	// note: does not use `foo()` at all
}

setTimeout(bar,60000); // make bar's closure stay around awhile

{
	let foo = function foo() {
		// 1000+ LoC of a bunch of complex stuff
	};

	for (var i=0; i<10; i++) {
		foo(); // only places `foo()` is used
	}
}

Simple change, but potentially a big performance difference. Now, it's clear that foo() only exists inside that block. bar() wouldn't have access to it anyway, even if it tried, so foo() definitely doesn't need to be kept around while bar() is waiting to execute. The engine has a much clearer signal that it's OK to free up foo() from the closure, to garbage collect it.

Will the engines do this? I dunno. You dunno, probably. But they can. And they might. And they might be able to more efficiently than if you hadn't used block-scoping.

How does let-er work?

let-er will, by default, transform your let ( .. ) { .. } style blocks into standard ES6-compatible code.

This:

let (x = "foo") {
	console.log(x); // "foo"
}

console.log(x); // Reference Error!

Becomes this:

{ let x = "foo";
  console.log(x); // "foo"
}

console.log(x); // Reference Error!

Of course, once you have standard ES6 code, you can either run it directly in an ES6 compatible browser/environment, or you can then run the whole code base through a ES6-ES3/5 transpiler, so that you can target older environments.

In otherwords, let-er should be used as a preprocessor to bring your code to ES6 compliance, and then go from there as you normally would.

If you don't plan to use another ES6 transpiler, you can make let-er target ES3+ compatibility, such that the code produced would be:

try{throw "foo"}catch
/*let*/ (x /* = "foo" */) {
  console.log(x); // "foo"
}

console.log(x); // Reference Error!

A bit hacky? Sure. But it works. And you get to write nice, maintable code with the benefits of block-scoping. Who cares what the transpiled code looks like, right? :)

NOTE: By default, shown here are the extra comment annotations to make the transpiled code a bit more readable. You can suppress these default comment annotations with the annotate option setting, in which case the ES3+ code produced would be:

try{throw "foo"}catch(x){
  console.log(x); // "foo"
}

console.log(x); // Reference Error!

Multiple let-declarations

Multiple let-declarations are supported:

let (x = "foo", y = "bar") {
  console.log(x, y); // "foo" "bar"
}

console.log(x, y); // Reference Error!

The ES6 transpiled code will look like this:

{ let x = "foo", y = "bar";
  console.log(x, y); // "foo" "bar"
}

console.log(x, y); // Reference Error!

The ES3+ transpiled code will look like this:

try{throw "foo"}catch
/*let*/ (x /* = "foo" */) {
try{throw "bar"}catch
/*let*/ (y /* = "bar" */) {
  console.log(x, y); // "foo" "bar"
}}

console.log(x, y); // Reference Error!

Or, ES3+ without annotations:

try{throw "foo"}catch(x){try{throw "bar"}catch(y){
  console.log(x, y); // "foo" "bar"
}}

console.log(x, y); // Reference Error!

Performance

Of course, the ES6 transpilation has zero performance difference to regular ES6.

But, you may be wondering if there's some crazy performance hit to the ES3 try..catch hack. So, here's several thoughts to address that concern:

  1. Comparing block-scoped code to non-block-scoped code performance-wise wouldn't really tell you what you might expect. You might intuitively be wondering if the performance hit is small enough (or zero!) so that you can use block-scoping "for free". It certainly is not.

    Either you need/want block-scoping, or you don't. But asking if using block-scoping is poorer performance than not using block-scoping is not a terribly useful question. The merits of the block-scoping functionality are the important question.

  2. A fairer comparison, though still skewed, is comparing different approaches for getting "block scoping". The main alternative to the try..catch hack which let-er uses is the IIFE (creating an inline, auto-executing function to get an extra function scope block where you want block-scoping). So, you can examine how try..catch compares to IIFE.

    As you'll see there, try..catch is about 10-20% slower on average than an IIFE. This may seem like the nail in the coffin for this approach. However, there's a major caveat to using an IIFE. It has some significant side-effects.

    Namely, the value of this, the meaning of return break continue, and other such things, are very different inside an IIFE, whereas these things are not affected by wrapping a catch(..) { .. } block around any arbitrary code.

    So, even the IIFE vs. try..catch isn't a totally fair comparison, but it's close enough to be relevant. It does show that the try..catch hack isn't orders of magnitude worse or anything crazy like that. It's a bit worse than an IIFE, but in exchange you get truer block-scoping that is less intrusive/destructive to the code.

  3. This transpiling to ES3 via try..catch is an admitted polyfill. Polyfills almost always have worse performance than their newer native counterparts. ES6 is giving us the let keyword for real block-scoping, and it certainly will be faster than the polyfill hack.

    Fortunately, let-er defaults to ES6 code if you happen to only care about ES6 environments (node.js for instance). As you can see above, the ES6-only targeting transpiles to usage of the native let ..; declaration syntax, which should have all the best native performance you can get.

API

The compile(..) API method takes code and detects if there are any matching let ( .. ) { .. } style blocks that it needs to transpile. You get a single code string back, ready to go.

letEr.compile('let(x="foo"){console.log(x);}');

Additional API methods

The API also includes lex(..), parse(..), and generate(..):

  • lex(..) takes code and returns an array of tokens from the lexing of the code.
  • parse(..) takes the array of tokens and parses it for let-blocks, and returns an AST (tree: an array of nodes and nested nodes).
  • generate(..) takes the AST formatted as it comes from parse(..) and produces the code string, the same as it comes from the compile(..) API method.

If you inspect the token stream or the AST, each element has a type attribute with a numeric value. The let-er API provides constants for these values to make it easier to interpret the element types.

For example, letEr.TOKEN.LET_HEADER is the value that identifies the token list (nested tokens) that represents the let-block's declaration header. letEr.NODE.LET_BLOCK by contrast is the value for a node in the AST that represents a full let-block, including the nested declarations and the content inside the { .. } block.

let-er uses literalizer, which means that the token stream and the AST will have each of literalizer's identified literals as separate elements. Those elements all have a literal property with a value that corresponds to the constants on the literalizer API, such as LIT.SEGMENT.STRING_LITERAL for, obviously, string literal elements.

Most people will not need to use these additional let-er API methods, but if you do need to perform extra analysis or transforms, the API provides you that flexibility. NOTE: Be careful to not modify/invalidate the format of these element structures, or let-er will likely not be able to consume them again for the next step.

CLI

let-er comes with a node.js tool called leter (note the lack of -), in the "bin/" directory, which is a CLI tool for compiling your let-block JS code.

Here are the options for the CLI tool:

usage: leter opt [, ..opt]

options:
--help                 show this help

--ignore-warnings      ignore and suppress any warnings
--no-annotate          do not annotate ES3 output
--es6                  target ES6 for output (use native let keyword)

--compile              compile the JS stream from 'stdin'
--compile=file         compile a JS file

If the --compile flag is passed, it will read code in from stdin.

The --compile=file flag can (also) be passed multiple times, once for each file you want to compile.

bin/leter --compile=some/file1.js --compile=some/file2.js
echo ".. some code .." | bin/let --compile --compile=some/file3.js

Options

There are two options you can set that control the type of code produced by let-er (aka, the generate(..) step).

  • letEr.opts.es3 (boolean; default: false) - If set to true, will target ES3+ environments with the try{throw "foo"}catch(x){ .. } hack syntax.

  • letEr.opts.annotate (boolean; default: true) - If set to true, will output additional code comments (as shown in the above example snippets) to annotate to make the ES3+ compiled code more readable/trackable to the original code. Otherwise, only the bare minimum code will be output.

    NOTE: This option only matters if the es3 option is set, and will have no effect otherwise.

Warnings

If there are any warnings/notices produced as part of the lexing or let processing, they will be populated in the letEr.warnings array. You can call letEr.reset() to clear out the warnings list in between separate compilations.

License

The code and all the documentation are released under the MIT license.

http://getify.mit-license.org/

More Repositories

1

You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
178,746
star
2

Functional-Light-JS

Pragmatic, balanced FP in JavaScript. @FLJSBook on twitter.
JavaScript
16,593
star
3

LABjs

Loading And Blocking JavaScript: On-demand parallel loader for JavaScript with execution order dependencies
HTML
2,277
star
4

asynquence

Asynchronous flow control (promises, generators, observables, CSP, etc)
JavaScript
1,743
star
5

CAF

Cancelable Async Flows (CAF)
JavaScript
1,332
star
6

monio

The most powerful IO monad implementation in JS, possibly in any language!
JavaScript
1,049
star
7

TNG-Hooks

Provides React-inspired 'hooks' like useState(..) for stand-alone functions
JavaScript
1,011
star
8

native-promise-only

A polyfill for native ES6 Promises as close as possible (no extensions) to the strict spec definitions.
JavaScript
723
star
9

A-Tale-Of-Three-Lists

Comparing various async patterns for a single demo
JavaScript
651
star
10

fasy

FP iterators that are both eager and asynchronous
JavaScript
546
star
11

FPO

FP library for JavaScript. Supports named-argument style methods.
JavaScript
449
star
12

youperiod.app

YouPeriod.app -- the privacy-first period tracking app
JavaScript
442
star
13

JSON.minify

Simple minifier for JSON to remove comments and whitespace
409
star
14

TypL

The Type Linter for JS
JavaScript
374
star
15

h5ive-DEPRECATED

**DEPRECATED** A collection of thin facade APIs wrapped around HTML5 JavaScript features.
JavaScript
324
star
16

eslint-plugin-proper-arrows

ESLint rules to ensure proper arrow function definitions
JavaScript
305
star
17

foi-lang

Foi: a different kind of functional programming language
JavaScript
303
star
18

grips

Simple-logic templates
JavaScript
287
star
19

moduloze

Convert CommonJS (CJS) modules to UMD and ESM formats
JavaScript
207
star
20

ES-Feature-Tests

Feature Tests for JavaScript
JavaScript
199
star
21

Incomplete-JS

"The Incomplete Guide to JavaScript" (book). @IncompleteJS on twitter.
190
star
22

revocable-queue

Specialized async queue data structure, supports revocation of values
JavaScript
176
star
23

deePool

highly-efficient pool for JavaScript objects
JavaScript
118
star
24

getify.github.io

JavaScript
113
star
25

concrete-syntax-tree

Defining a standard JavaScript CST (concrete syntax tree) to complement ASTs
107
star
26

eslint-plugin-proper-ternary

ESLint rules to ensure proper usage of ternary/conditional expressions
JavaScript
96
star
27

cloud-sweeper

A casual game built for the web.
JavaScript
94
star
28

BikechainJS

JavaScript VM engine (powered by V8); server-side environment modules; server-side synchronous web app controllers
C++
82
star
29

wepuzzleit

Demo PoC game for various advanced HTML5 js APIs
JavaScript
79
star
30

workshop-periodic-table

66
star
31

elasi

EL/ASI: Encrypt Locally, Account Secure Interchange
JavaScript
62
star
32

remote-csp-channel

Remote bridge for CSP channels
JavaScript
55
star
33

ScanTree

Scan a JS file tree to build an ordered and grouped dependency listing
JavaScript
51
star
34

dwordly-game

A game where words dwindle down to the shortest possible
JavaScript
42
star
35

stable-timers

timers with less race conditions
JavaScript
38
star
36

emdash

Simple blogging with node/iojs + GitHub.
36
star
37

domio

DOM and Event helpers for Monio
JavaScript
30
star
38

eslint-plugin-no-optional-call

ESLint plugin to disallow the optional-call operator
JavaScript
30
star
39

eslint-plugin-arrow-require-this

DEPRECATED: ESLint rule to require arrow functions to reference the 'this' keyword
JavaScript
28
star
40

import-remap

Rewrite ES module import specifiers using an import-map.
JavaScript
25
star
41

gum-try-hd

Try to enforce HD (16:9) camera aspect ratio for web-video calls
JavaScript
25
star
42

Mock-DOM-Resources

A mock of (parts of) the DOM API to simulate resource preloading and loading
JavaScript
25
star
43

make-a-game

some project files for a tutorial on making a simple web game
JavaScript
25
star
44

mpAjax

framework plugin for handling multi-part Ajax responses
JavaScript
23
star
45

asyncGate.js

DEPRECATED. asynchronous gate for javascript
JavaScript
21
star
46

tic-tac-toe-workshop

Workshop files for building tic-tac-toe in JS and <canvas>
21
star
47

esre

esre: fully configurable JS code formatting
20
star
48

workshop-chess-diagonals

17
star
49

featuretests.io

JavaScript Feature Tests... as a service
JavaScript
16
star
50

FoilScript

FoilScript: a new dialect of JS that fixes the sucky parts but still looks and feels like JS
16
star
51

literalizer

Specialized heuristic lexer for JS to identify complex literals
JavaScript
16
star
52

normalize-selector

Normalize CSS selectors
JavaScript
14
star
53

DOMEventBridge

Bridge DOM events to a JS event hub (for pubsub)
JavaScript
14
star
54

pong-around-workshop

Workshop files for building a pong-variant game in JS and <canvas>
12
star
55

workshop-wordy-unscrambler

11
star
56

shortie.me

Proof-of-concept demo for server-side JavaScript driven "middle-end" architecture
JavaScript
11
star
57

workshop-knights-dialer

8
star
58

middleend-boilerplate

Boilerplate Starting Point for Middle-end Web Architecture
JavaScript
8
star
59

demo-app-weatheround

JavaScript
7
star
60

the-economy-of-keystrokes-slides

Slides code built for "The Economy of Keystrokes" talk
JavaScript
6
star
61

santa-connect-app

Santa Connect: keeping track of your kids' nice/naughty status
JavaScript
5
star
62

nyc-bug-demo

bug demo for NYC code coverage tool
JavaScript
4
star
63

unnamed

unnamed (for now). nothing to see here. ;-)
JavaScript
2
star
64

my-lifesheets

CSS
1
star
65

breakthewebforward.com

JavaScript
1
star