I've tried to get V8 at least to implement smarter TDZ elision for a long time, which would eliminate the need for these shenanigans.
There are some relatively simple heuristics where you can tell without escape analysis that a variable will not be referenced before initialization.
The obviously bad constructions are references in the same scope that happen before the declaration. It'd be nice if these were an early errors, but alas, so keep the TDZ check. The next is any closed over reference that happens before the initializer. These may run before the initializer, so keep the TDZ check. Then you have hoisted closures even if they're after the initializer (eg, var and function keyword declarations). These might run before the initializer too.
But everything else that comes after the initializer: access in the same or nested scope and access in closures in non-hoisted declarations, can't possibly run before the initializer and doesn't need the TDZ check.
I believe this check is cheap enough to run during parsing. The reason for not pursuing it was that there wasn't a benchmark that showed TDZ checks were a problem. But TypeScript showed they were!
Indeed, `let`s and `const`s incur a significant performance penalty. This is also why the Scala.js compiler emits `var`s by default, even when targeting very recent versions of ECMAScript.
The good news is that we can still write our Scala `val`s and `var`s (`const` and `let`) in the source code, enjoying good scoping and good performance.
`let`s and `const`s incur a significant performance penalty.
Is that still true? Early versions of V8 would do scope checks for things that weren't declared with var but it doesn't do that any more. I think const and let are lowered to var representation at compile time now anyway, so when the code is running they're the same thing.
And here I was thinking I should finally embrace using const and let for performance reasons .. shouldn't in theory the compiler has more room for optimization if it knows the variable won't be changed? Or is apperently all the scope checking more expensive?
The first example is not “terrible”, that’s just how lexical scope works. I don’t really see the point of complaining about language features like this - either learn how it works or ignore at your peril.
That's not how lexical scope works anywhere but in JavaScript. Or rather, it's the interaction between "normal" lexical scope and hoisting. In a "normal" lexically scoped language, if you tried:
function f() {
return x; // Syntax parsing fails here.
}
let x = 4;
return f();
you would get the equivalent of a ReferenceError for x when f() tried to use it (well, refer to it) at the commented line. But in JavaScript, this successfully returns 4, because `let` inherits the weird hoisting behavior of `var` and `function`. And it has to, because otherwise this would be really weird:
function f1() { return x; }
let x = 4;
function f2() { return x; }
return Math.random() < 0.5 ? f1() : f2();
Would that have a 50/50 chance of returning the outer x? Would the engine have to swap which x is referred to in f1 when x gets initialized?
TDZ is also terrible because the engines have to look up at runtime whether a lexical variable's binding has been initialized yet. This is one reason (perhaps the main reason?) why they're slower. You can't constant fold even a `const`, because `const v = 7` means at runtime "either 7 or nothing at all, not even null or undefined".
In my opinion, TDZ was a mistake. (Not one I could have predicted at the time, so no shade to the designers.) The right thing to do when introducing let/const would have been to make any capture of a lexical variable disable hoisting of the containing function. So the example from the article (trimmed down a little)
return Math.random() < 0.5 ? useX() : 1;
let x = 4;
function useX() { return x; }
would raise a ReferenceError for `useX`, because it has not yet been declared at that point in the syntactic scope. Same with the similar
return Math.random() < 0.5 ? x : 1;
let x = 4;
which in current JavaScript also either returns 1 or throws a ReferenceError. I'm not against hoisting functions, and removing function hoisting would have not been possible anyway. The thing is, that's not "just a function", that's a closure that is capturing something that doesn't exist yet. It's binding to something not in its lexical scope, an uninitialized slot in its static environment. That's a weird special case that has to be handled in the engine and considered in user code. It would be better to just disallow it. (And no, I don't think it would be a big deal for engines to detect that case. They already have to compute captures and bindings.)
I think it's reasonable to have the opinion that the way lexical scoping works in JS is "terrible". You may disagree, but "that's just how it works" isn't a good argument. That line of reasoning is often a rationalization that we make when we are very used to a technology - a sort of hostage situation.
I second that, I actually don't understand why do people believe every pair of curly braces has to be its own separate scope. An explicit construct for scoping would have been so much clearer to me.
It feels like the root of the issue is the scoping design of JS itself, which makes tracking TDZ more costly for the interpreter, and the fact that JS is JIT rather than AOT compiled.
I laud the recent efforts to remove the JS from JS tools (Go in TS compiler, esbuild, etc), as you don't need 100% of your lang utils written in the same interpreted lang, especially slow/expensive tasks like compilation.
Considering anything that transpiled to ES5 would have to use var anyway, I'm curious why this was done in the source itself and not as a plugin/build step.
> As of TypeScript 5.0, the project's output target was switched from es5 to es2018 as part of a transition to ECMAScript modules. This meant that TypeScript could rely on the emit for native (and often more-succinct) syntax supported between ES2015 and ES2018. One might expect that this would unconditionally make things faster, but surprise we encountered was a slowdown from using let and const natively!
So they don't transpile to ES5, and that is the issue.
I wish they had made lexical scope work like Lua, where the binding simply does not exist before the declaration – including in the initialization expression:
local x = 42
do
print(x) -- 42
local x = x * 2
print(x) -- 84
end
print(x) -- 42
Need some help understanding what’s going on here.
In
function example(measurement) {
console.log(calculation); // undefined - accessible! calculation leaked out
console.log(i); // undefined - accessible! i leaked out
<snip>
Why does the author say `calculation` and `i` are leaking? They’re not even defined at that point (they come later in the code), and we’re seeing “undefined” which, correct me if I’m wrong, is the JS way of saying “I have no idea what this thing is”. So where’s the leakage?
Two spaces before each line in the code block. HN doesn't use markdown, it's easy to do even on mobile, a demonstration:
function example(measurement) {
console.log(calculation); // undefined - accessible! calculation leaked out
console.log(i); // undefined - accessible! i leaked out <snip>
It's "leaking" because the variable is in scope, it's associated value is "undefined". This is different than with let/const where the variable would not be in scope at that point in the function. An undefined value bound to a variable is not the same as "I have no idea what this thing is". That would be the reference errors seen with let/const.
The article is slightly wrong. The TDZ is the zone before the variable is declared, not after. Referencing variables before they're declared isn't valid for `let` and hence it needs a TDZ check.
Consider
console.log(foo)
let foo
vs
console.log(foo)
var foo
I think the article confuses "in scope" with "declared", and "declared and initialised" with "initialised".
No, the crux of the article is that using var instead of let or const can produce a performance improvement by reducing the complexity of what the interpreter must track.
They cite a surprising 8% performance boost in some cases by using var.
Yes. But what are you implying by the word "just"? It sounds like you're saying we should be taking something different away from the article's description of this behavior simply because you have put a name to it.
[+] [-] spankalee|5 months ago|reply
There are some relatively simple heuristics where you can tell without escape analysis that a variable will not be referenced before initialization.
The obviously bad constructions are references in the same scope that happen before the declaration. It'd be nice if these were an early errors, but alas, so keep the TDZ check. The next is any closed over reference that happens before the initializer. These may run before the initializer, so keep the TDZ check. Then you have hoisted closures even if they're after the initializer (eg, var and function keyword declarations). These might run before the initializer too.
But everything else that comes after the initializer: access in the same or nested scope and access in closures in non-hoisted declarations, can't possibly run before the initializer and doesn't need the TDZ check.
I believe this check is cheap enough to run during parsing. The reason for not pursuing it was that there wasn't a benchmark that showed TDZ checks were a problem. But TypeScript showed they were!
[+] [-] syg|5 months ago|reply
[+] [-] sjrd|5 months ago|reply
The good news is that we can still write our Scala `val`s and `var`s (`const` and `let`) in the source code, enjoying good scoping and good performance.
[+] [-] onion2k|5 months ago|reply
Is that still true? Early versions of V8 would do scope checks for things that weren't declared with var but it doesn't do that any more. I think const and let are lowered to var representation at compile time now anyway, so when the code is running they're the same thing.
[+] [-] lukan|5 months ago|reply
[+] [-] csto12|5 months ago|reply
[+] [-] taejavu|5 months ago|reply
[+] [-] sfink|5 months ago|reply
TDZ is also terrible because the engines have to look up at runtime whether a lexical variable's binding has been initialized yet. This is one reason (perhaps the main reason?) why they're slower. You can't constant fold even a `const`, because `const v = 7` means at runtime "either 7 or nothing at all, not even null or undefined".
In my opinion, TDZ was a mistake. (Not one I could have predicted at the time, so no shade to the designers.) The right thing to do when introducing let/const would have been to make any capture of a lexical variable disable hoisting of the containing function. So the example from the article (trimmed down a little)
would raise a ReferenceError for `useX`, because it has not yet been declared at that point in the syntactic scope. Same with the similar which in current JavaScript also either returns 1 or throws a ReferenceError. I'm not against hoisting functions, and removing function hoisting would have not been possible anyway. The thing is, that's not "just a function", that's a closure that is capturing something that doesn't exist yet. It's binding to something not in its lexical scope, an uninitialized slot in its static environment. That's a weird special case that has to be handled in the engine and considered in user code. It would be better to just disallow it. (And no, I don't think it would be a big deal for engines to detect that case. They already have to compute captures and bindings.)Sadly, it's too late now.
[+] [-] happytoexplain|5 months ago|reply
[+] [-] throw-the-towel|5 months ago|reply
[+] [-] bigstrat2003|5 months ago|reply
[+] [-] twistedpair|5 months ago|reply
I laud the recent efforts to remove the JS from JS tools (Go in TS compiler, esbuild, etc), as you don't need 100% of your lang utils written in the same interpreted lang, especially slow/expensive tasks like compilation.
[+] [-] adzm|5 months ago|reply
[+] [-] inbx0|5 months ago|reply
> As of TypeScript 5.0, the project's output target was switched from es5 to es2018 as part of a transition to ECMAScript modules. This meant that TypeScript could rely on the emit for native (and often more-succinct) syntax supported between ES2015 and ES2018. One might expect that this would unconditionally make things faster, but surprise we encountered was a slowdown from using let and const natively!
So they don't transpile to ES5, and that is the issue.
1: https://github.com/microsoft/TypeScript/issues/52924
[+] [-] pwdisswordfishy|5 months ago|reply
[+] [-] Goofy_Coyote|5 months ago|reply
In
Why does the author say `calculation` and `i` are leaking? They’re not even defined at that point (they come later in the code), and we’re seeing “undefined” which, correct me if I’m wrong, is the JS way of saying “I have no idea what this thing is”. So where’s the leakage?[+] [-] Jtsummers|5 months ago|reply
[+] [-] conartist6|5 months ago|reply
[+] [-] strken|5 months ago|reply
Consider
vs I think the article confuses "in scope" with "declared", and "declared and initialised" with "initialised".[+] [-] pverheggen|5 months ago|reply
[+] [-] benatkin|5 months ago|reply
I can always rely on FAANGs to make things unnecessarily confusing and ugly.
[+] [-] sorrythanks|5 months ago|reply
[+] [-] craftkiller|5 months ago|reply
[+] [-] Aurornis|5 months ago|reply
They cite a surprising 8% performance boost in some cases by using var.
[+] [-] happytoexplain|5 months ago|reply