ES6 let. More memory?

If let is creating new scopes for variables to live in, does that mean a greater memory footprint for executing code? Consider a loop containing a function which will capture each of the scopes for each loop…

let fns = [];
for (let i = 0; i < 100000; i++) {
   fns.push(function(){
      // ...
   });
}
fns; // 100000 captured scopes?

If not using i in that function, would it be more efficient to be using var for the i variable thereby only allowing one scope to be captured (or none, since a new one, I presume, would not be created for the for block)?

lets don’t create a scope; they just create variable bindings within block scopes (lexical environments) rather than within function scopes (variable environments), but both block and function scopes exist either way. (That may sound a bit picky, but scopes in non-with JavaScript are lexical scopes, which means they’re determined during the lexing phase of parsing, and don’t necessarily have any runtime representation.)

If you have a nested function, it will capture the chain of lexical and variable environments, which is sort of the same thing as saying that the resultant closure closes-over those let, const, and var bindings.

for(let ...) loops create a new lexical environment, unlike for(var ...) loops, so the nested function in your example would hypothetically capture a unique binding of i in each iteration of the loop, which leads to the memory concerns you cited. (This is actually fairly unique to the loop form. Normally, let itself doesn’t create a lexical environment, blocks do.)

The efficiency is up to the implementation. I think the case of not even using a captured variable is pretty easy to statically detect, in the absence of eval, or running in a debug mode which tries to let you look at normally-invisible structures.

The spec is consistently insistent that things like lexical environments and execution contexts don’t necessarily need to translate into actual, in-memory structures in an implementation, so if an implementation can get away with unobservably not tracking certain state, that’s fair game.

when the terms “LexicalEnvironment”, and “VariableEnvironment” are used without qualification they are in reference to those components of the running execution context.

An execution context is purely a specification mechanism and need not correspond to any particular artefact of an ECMAScript implementation. It is impossible for ECMAScript code to directly access or observe an execution context.

So you could test what current ES6 implementations do by checking out memory usage differences between let/const and var, or by looking at the source code. My hunch is that they don’t optimize it yet, but will quickly when usage of ES6 ramps up.


There’s a relevant discussion on es-discuss here: In ES6, do for loops with a let/const initializer create a separate scope?


I currently get different output for this code in Chromium 43 and FF 36, so keep that in mind:

void function(){ 
	'use strict'
	let fns = []
	for(let i = 0; i < 5; i++){ 
		fns.push(function(){ console.log(i) }) 
	}
	fns[3]()
}() // (3 in V8, 5 in SpiderMonkey)

I’m pretty sure that V8’s interpretation is the correct one according to the most recent ES6 spec draft I’ve seen.

Great information, thanks!

Something else I considered was that my particular use case may not be very useful anyway. I see very little reason to create separate function instances in a loop if those functions aren’t going to be capturing anything uniquely defined in that loop. The only exception I can think of being adding custom properties to the function object itself, for whatever reason you might have to do so.

I noticed that before - yesterday, I think - when trying the same code. I agree that V8 is correct.

Something else I noticed what that you can shadow loop variables in the loop body in both ff and chrome, though babeljs throws an error. I’m pretty sure this is allowed so likely just a bug in babel.

!function(){"use strict"
for (let i = 0; i<3; i++){
  let i = 5;
  console.log(i); // three logs of: 5
}
}()

Well, you might want the convenience of using a lambda/fat-arrow or functional-style array method without hoisting that function instance outside the loop. Even with long-form functions, you might want to do the same thing but keep the semantics of this compared to fat arrow functions.

Yup that’s covered in the es-discuss link I cited. :trout: Unless the spec for that changed in the last 9 months.

yeah I found that one earlier when looking into this too. Wasn’t sure if babel, which seems pretty popular right now, is stumbling or something else. I guess mentioning it was pretty redundant since the link pretty much explains this very behavior :trout: (then again I was referencing babel which was new :wink: )