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.
I'm sure it can do that in many cases. But if the scopes are a bit complicated, and in particular when variables are captured in lambdas, it's just not possible. The semantics require the TDZ behavior. If you can statically analyze that the TDZ won't be triggered, you can lower to `var`, but otherwise you have to keep the checks.
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?
Usage of Scala.js is steadily growing. Several indicators suggest that 1 in 5 Scala developers use Scala.js at this point. It's regularly brought up as one of the strongest suits of Scala.
Usage of Scala itself is less shiny if you look at market share. But I believe it's still growing in absolute numbers, only quite slowly.
>Usage of Scala.js is steadily growing. Several indicators suggest that 1 in 5 Scala developers use Scala.js at this point.
Wow! 4 out of 20 total ain't bad!
Jocking aside the starting set (Scala developers) is already small, so it's not like either it or even less Scala.js is going to be a major player anytime soon.
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.)
That’s not the example I’m talking about. I mean where he defines `calculation` within the curly braces of the if statement, then says it “leaked out” because he can log it below the closing brace of the if statement. That’s a perfect example of the difference between lexical scope and block scope.
There are several examples in the blog, and only one is the first. It does not include the "terrible" descriptor after it. So your comment is kind of odd because it doesn't connect to the article at all.
If you mean the first example that's described as "terrible", that's the second example and it's the one with the leaking loop variable. It kind of is terrible, Python has the same problem (and many others, Python scoping rules are not good). C used to have that problem but they at least had the good sense to fix it.
> the difference between lexical scope and block scope
There isn't a difference between lexical scope and "block" scope. What I think you are referring to as "block" scope, is a subset of lexical scope. The difference between var and let/const is where the boundaries of the lexical scope is.
Ah, fair, I didn't actually pay attention to which example you were referring to. That example is specifically about `var` being terrible, not `let/const`.
I was really using your comment as a jumping off point for my rant.
I wouldn't describe `var` declarations as lexical, though. Sure, they have a lexical scope that they get hoisted up to cover, but hoisting is not "just lexical scope". It's unusual.
One thing about scope hoisting in JS is that it allows to simulate some aspects of functional programming, where the the order of execution does not necessarily math the order of declaration. I use this all the time to order code elements in a file in order of decreasing importance.
I wish there was explicit support for this though, maybe with a construct like <exression> where <antecedent>, etc, like what Haskell has, instead of having to hack it using functions and var
This is not some terrible decision that comes with only downsides. In fact there are quite a few upsides to the flexibility it brings compared to a language like Python that works as you describe.
It basically means you can always override anything, which allows for monkey patching and proxying and adapter patterns and circular imports… These are all nasty things to accidentally encounter, but they can also be powerful tools when used deliberately.
These hoisting tricks all play an important role in ensuring backwards compatibility. And they’re the reason why JavaScript can have multiple versions of the same package while Python cannot.
Actually, let/const do the opposite of adding flexibility. Simple example: if you have a REPL, it has to cheat (as in, violate the rules of the language) in order to do something sensible for let/const. Once you do `let x;`, you can never declare `x` in the REPL again. In fact, simple typing `console.log(v)` is ambiguous: will you enter `let v` some time in the future or not?
You can't monkey patch lexicals, that is much of their point. Any reference to a lexical variable is a fixed binding.
In practice, this comes up most often for me when I have a script that I want to reload: if there are any toplevel `let/const`, you can't do it. Even worse, `class C { ... }` is also a lexical binding that cannot be replaced or overridden. Personally, I normally use `var` exclusively at the toplevel, and `let/const` exclusively in any other scope. But `class` is painful -- for scripts that I really want to be able to reload, I use `var C = class { ... };` which is fugly but mostly works. And yet, I like lexical scoping anyway, and think it's worth the price. The price didn't have to be quite so high, is all. I would happily take the benefit of avoiding TDZ for the price of disabling hoisting in the specific situations where it no longer makes sense.
I agree that hoisting is a backwards compatibility thing. I just think that the minute optional lexical scoping entered the picture, hoisting no longer made sense. Either one is great, the combination is awful, but it's possible for them to coexist peacefully in a language if you forbid the problematic intersection. TDZ is a hack, a workaround, not peaceful coexistence. (TDZ is basically the same fix as I'm proposing, just done dynamically instead of statically. Which means that JS's static semantics depend on dynamic behavior, when the whole point of lexical scoping is that it's lexical.)
> if there are any toplevel `let/const`, you can't do it [monkeypatch it]
True, but you can at least wrap the entire scope and hack around it. It's not gonna be pretty or maintainable but you can avoid/override the code path that defines the let.
Anecdotally... I've monkeypatched a lot of JavaScript code and I've never been stopped from what I wanted to do, whereas with Python I've hit a dead-end in similar situations. Maybe there's some corner case that's unpatchable but I really think there is always a workaround by the ability to wrap the scope in a closure. Worst case you re-implement the entire logic and change the bit you care about.
None of the things you mentioned are clearly related to each other. “It basically means you can always override anything, which allows for monkey patching and proxying and adapter patterns and circular imports” is not true. “They’re the reason why JavaScript can have multiple versions of the same package while Python cannot” is definitely not true. (I’m not even sure if you’re referring to TDZ or hoisting or lexical scope or whatever other part of the context, but these things are unrelated to every option.)
The premise of “a language like Python that works as you describe” is wrong too, since Python doesn’t work like that (it has the same hoisting and TDZ concepts as JavaScript):
def g():
def f():
return x
x = 4
return f()
print(g()) # 4
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.
> I actually don't understand why do people believe every pair of curly braces has to be its own separate scope.
It’s much easier to reason about when your variables aren’t going to escape past the end of the block.
In non-GC languages going out of scope can also be a trigger to free the contents of the variable. This is useful for situations like locking where you can put the minimal span of code that requires the lock into a scope and take a lock which automatically unlocks at the end of the scope, for example.
JavaScript’s hoisting and scoping feel natural to people who started in JS, but most people who came from other languages find it surprising.
GCed languages often also have a mechanism for running code on scope exit. For example Lua's <close> attribute, which runs a metamethod when it goes out of scope rather than having to wait for GC:
function readfile(name)
local f <close> = assert(io.open(name))
return assert(f:read"a")
end
In most languages, each block indeed is a separate scope. And it avoids foot guns about accidentally using variables that already serve another purpose. I guess it's one of the things that are typical for dynamic languages.
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
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".
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.
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.
By crux you mean the 1 paragraph at the end where it mentions performance? That's basically a footnote to an article that spends the other 99% describing javascript variable hoisting. They cite an 8% performance boost but they don't analyze it, instead just claiming it is a lot of work for the interpreter and linking to a github issue. They've run no benchmarks. They have shown no interpreter internals. They just report that one project saw an 8% performance improvement.
They did a great job of explaining javascript variable hoisting, but that's all that they have explained.
> By crux you mean the 1 paragraph at the end where it mentions performance? That's basically a footnote to an article that spends the other 99% describing javascript variable hoisting.
Isn't that part still the crux of the article as it contains the answer to the title?
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.
Think of it like a tl;dr. Hoisting is common knowledge to javascript programmers, so I've managed to compress the information of this article into 6 words for them.
They are only talking about one javascript engine (node). They didn't test any other engine or go into the implementation in node. For all we know, this might just be a poorly optimized code path in node that needs a little love, but the author didn't bother doing any investigation.
Looking at the linked github issue, jsc doesn't have the performance penalty. It would have been interesting if the author had investigated why and shared that with us.
If this article is about hoisting then this is a well-made high-effort high-value article. If this article is about performance then this is a low-effort low-value summary of a github issue they read.
Maybe it could be optimized more but the TDZ fundamentally adds a performance cost, because it requires a runtime check to see if the variable has been initiatalized yet.
Also node used the v8, the same engine as chromium. So this doesn't just affect node, it also affects the majority of the browser share market. oh, and deno uses v8 too.
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!
This kind of elision is implemented.
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.
I'm sure it can do that in many cases. But if the scopes are a bit complicated, and in particular when variables are captured in lambdas, it's just not possible. The semantics require the TDZ behavior. If you can statically analyze that the TDZ won't be triggered, you can lower to `var`, but otherwise you have to keep the checks.
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?
I wonder how many companies are still using Scala.js. Scala was fun to work with, wish it was more popular these days.
Usage of Scala.js is steadily growing. Several indicators suggest that 1 in 5 Scala developers use Scala.js at this point. It's regularly brought up as one of the strongest suits of Scala.
Usage of Scala itself is less shiny if you look at market share. But I believe it's still growing in absolute numbers, only quite slowly.
>Usage of Scala.js is steadily growing. Several indicators suggest that 1 in 5 Scala developers use Scala.js at this point.
Wow! 4 out of 20 total ain't bad!
Jocking aside the starting set (Scala developers) is already small, so it's not like either it or even less Scala.js is going to be a major player anytime soon.
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:
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: 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)
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.
That’s not the example I’m talking about. I mean where he defines `calculation` within the curly braces of the if statement, then says it “leaked out” because he can log it below the closing brace of the if statement. That’s a perfect example of the difference between lexical scope and block scope.
>>> The first example is not “terrible”
There are several examples in the blog, and only one is the first. It does not include the "terrible" descriptor after it. So your comment is kind of odd because it doesn't connect to the article at all.
If you mean the first example that's described as "terrible", that's the second example and it's the one with the leaking loop variable. It kind of is terrible, Python has the same problem (and many others, Python scoping rules are not good). C used to have that problem but they at least had the good sense to fix it.
You’re right about my mistake, I should have said “the second code snippet”.
> the difference between lexical scope and block scope
There isn't a difference between lexical scope and "block" scope. What I think you are referring to as "block" scope, is a subset of lexical scope. The difference between var and let/const is where the boundaries of the lexical scope is.
Ah, fair, I didn't actually pay attention to which example you were referring to. That example is specifically about `var` being terrible, not `let/const`.
I was really using your comment as a jumping off point for my rant.
I wouldn't describe `var` declarations as lexical, though. Sure, they have a lexical scope that they get hoisted up to cover, but hoisting is not "just lexical scope". It's unusual.
One thing about scope hoisting in JS is that it allows to simulate some aspects of functional programming, where the the order of execution does not necessarily math the order of declaration. I use this all the time to order code elements in a file in order of decreasing importance.
I wish there was explicit support for this though, maybe with a construct like <exression> where <antecedent>, etc, like what Haskell has, instead of having to hack it using functions and var
This is not some terrible decision that comes with only downsides. In fact there are quite a few upsides to the flexibility it brings compared to a language like Python that works as you describe.
It basically means you can always override anything, which allows for monkey patching and proxying and adapter patterns and circular imports… These are all nasty things to accidentally encounter, but they can also be powerful tools when used deliberately.
These hoisting tricks all play an important role in ensuring backwards compatibility. And they’re the reason why JavaScript can have multiple versions of the same package while Python cannot.
Actually, let/const do the opposite of adding flexibility. Simple example: if you have a REPL, it has to cheat (as in, violate the rules of the language) in order to do something sensible for let/const. Once you do `let x;`, you can never declare `x` in the REPL again. In fact, simple typing `console.log(v)` is ambiguous: will you enter `let v` some time in the future or not?
You can't monkey patch lexicals, that is much of their point. Any reference to a lexical variable is a fixed binding.
In practice, this comes up most often for me when I have a script that I want to reload: if there are any toplevel `let/const`, you can't do it. Even worse, `class C { ... }` is also a lexical binding that cannot be replaced or overridden. Personally, I normally use `var` exclusively at the toplevel, and `let/const` exclusively in any other scope. But `class` is painful -- for scripts that I really want to be able to reload, I use `var C = class { ... };` which is fugly but mostly works. And yet, I like lexical scoping anyway, and think it's worth the price. The price didn't have to be quite so high, is all. I would happily take the benefit of avoiding TDZ for the price of disabling hoisting in the specific situations where it no longer makes sense.
I agree that hoisting is a backwards compatibility thing. I just think that the minute optional lexical scoping entered the picture, hoisting no longer made sense. Either one is great, the combination is awful, but it's possible for them to coexist peacefully in a language if you forbid the problematic intersection. TDZ is a hack, a workaround, not peaceful coexistence. (TDZ is basically the same fix as I'm proposing, just done dynamically instead of statically. Which means that JS's static semantics depend on dynamic behavior, when the whole point of lexical scoping is that it's lexical.)
> if there are any toplevel `let/const`, you can't do it [monkeypatch it]
True, but you can at least wrap the entire scope and hack around it. It's not gonna be pretty or maintainable but you can avoid/override the code path that defines the let.
Anecdotally... I've monkeypatched a lot of JavaScript code and I've never been stopped from what I wanted to do, whereas with Python I've hit a dead-end in similar situations. Maybe there's some corner case that's unpatchable but I really think there is always a workaround by the ability to wrap the scope in a closure. Worst case you re-implement the entire logic and change the bit you care about.
None of the things you mentioned are clearly related to each other. “It basically means you can always override anything, which allows for monkey patching and proxying and adapter patterns and circular imports” is not true. “They’re the reason why JavaScript can have multiple versions of the same package while Python cannot” is definitely not true. (I’m not even sure if you’re referring to TDZ or hoisting or lexical scope or whatever other part of the context, but these things are unrelated to every option.)
The premise of “a language like Python that works as you describe” is wrong too, since Python doesn’t work like that (it has the same hoisting and TDZ concepts as JavaScript):
Would mutual recursion still work with this solution? E.g.
It would work just like Lua then and require manually hoisting one of the declarations:
(Lua's named function statements are just syntatic sugar. For example `local function a()` is equivalent to `local a; a = function()`.)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.
In particular if it violates the assumptions of any non native programmer, then it's fair game for gripes.
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.
> I actually don't understand why do people believe every pair of curly braces has to be its own separate scope.
It’s much easier to reason about when your variables aren’t going to escape past the end of the block.
In non-GC languages going out of scope can also be a trigger to free the contents of the variable. This is useful for situations like locking where you can put the minimal span of code that requires the lock into a scope and take a lock which automatically unlocks at the end of the scope, for example.
JavaScript’s hoisting and scoping feel natural to people who started in JS, but most people who came from other languages find it surprising.
GCed languages often also have a mechanism for running code on scope exit. For example Lua's <close> attribute, which runs a metamethod when it goes out of scope rather than having to wait for GC:
>I actually don't understand why do people believe every pair of curly braces has to be its own separate scope
To avoid having to memorize yet one more thing that doesn't have an obvious benefit.
>An explicit construct for scoping would have been so much clearer to me
Having an additional construct for scoping is clearer than having every set of already-existing curly braces be a new scope? That seems backwards.
In most languages, each block indeed is a separate scope. And it avoids foot guns about accidentally using variables that already serve another purpose. I guess it's one of the things that are typical for dynamic languages.
> An explicit construct for scoping would have been so much clearer to me.
What would be the advantage over the system used everywhere else?
> An explicit construct for scoping would have been so much clearer to me.
Yes, maybe we could use something similar to parenthesis. Maybe they can look curly. /s
I mean, it certainly is surprising to me that you can use the variables before they are declared (albeit with undefined values).
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.
The post links to a TS issue [1] that explains
> 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
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:
Look, ma! No dead zones!Why wouldn't `let` be exactly what you want? It's block scoped but doesn't need fancy TDZ checks because like `var` it just starts out as undefined.
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
vs I think the article confuses "in scope" with "declared", and "declared and initialised" with "initialised".I think it’ll still throw a ReferenceError. Initialization is optional, but you still have to initialize before referencing.
Nope. `(() => {let bar; return bar})()` is `undefined`
Need some help understanding what’s going on here.
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?Two spaces before each line in the code block. HN doesn't use markdown, it's easy to do even on mobile, a demonstration:
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.Thanks for the tip, and also thanks for the explanation. I understand what’s going on here now. Much appreciated
To me it isn't unlike react having onChange={<function to be called when the input event fires>}
I can always rely on FAANGs to make things unnecessarily confusing and ugly.
i don't understand the connection
This is just javascript variable hoisting: https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
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.
By crux you mean the 1 paragraph at the end where it mentions performance? That's basically a footnote to an article that spends the other 99% describing javascript variable hoisting. They cite an 8% performance boost but they don't analyze it, instead just claiming it is a lot of work for the interpreter and linking to a github issue. They've run no benchmarks. They have shown no interpreter internals. They just report that one project saw an 8% performance improvement.
They did a great job of explaining javascript variable hoisting, but that's all that they have explained.
> By crux you mean the 1 paragraph at the end where it mentions performance? That's basically a footnote to an article that spends the other 99% describing javascript variable hoisting.
Isn't that part still the crux of the article as it contains the answer to the title?
Yes it turns out the article’s conclusion is in fact contained in the conclusion paragraph
To be fair, with JS's initialisation rules it does feel like it could have been anywhere.
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.
Think of it like a tl;dr. Hoisting is common knowledge to javascript programmers, so I've managed to compress the information of this article into 6 words for them.
The interesting part here is that javascript interpreters not having to track the TDZ comes with an interesting performance bonus.
javascript interpreter*
They are only talking about one javascript engine (node). They didn't test any other engine or go into the implementation in node. For all we know, this might just be a poorly optimized code path in node that needs a little love, but the author didn't bother doing any investigation.
Looking at the linked github issue, jsc doesn't have the performance penalty. It would have been interesting if the author had investigated why and shared that with us.
If this article is about hoisting then this is a well-made high-effort high-value article. If this article is about performance then this is a low-effort low-value summary of a github issue they read.
Maybe it could be optimized more but the TDZ fundamentally adds a performance cost, because it requires a runtime check to see if the variable has been initiatalized yet.
Also node used the v8, the same engine as chromium. So this doesn't just affect node, it also affects the majority of the browser share market. oh, and deno uses v8 too.