50 ms·
I've never liked the phrase 'variable hoisting'. It implies the compiler actively moves the variable declaration. What's actually happening is lexical scoping
by ajanuary 11y ago
I've never liked the phrase 'variable hoisting'. It implies the compiler actively moves the variable declaration.
What's actually happening is lexical scoping - a variable declaration is associated with a lexical scope. For var declarations, the lexical scope is the function, for let declarations, it's the block. When a variable is referenced, it first looks for the variable associated with the lexical scope of the variable reference, then walks up the chain of lexical scopes until it either finds a variable or hits the top scope and the variable doesn't exist.
Lexical scoping is easy to reason about and fairly easy to calculate. The majority of languages used today use lexical scope (the only exception in popular languages I can think of is perl which lets you use either lexical or dynamic scope, though I'm sure there are others).
A consequence of this design is that you can't have multiple variables with the same name in the same lexical scope. Most languages will raise an error if you redeclare a variable - javascript is unusual in that it doesn't.
Javascript is also unusual in that you can reference variables before they're both declared and definitely assigned. It's these design choices, interacting with lexical scope, that gives javascript such weird and notable behaviour that people have decided it needs a name - 'hoisting'.
So why are let variables 'hoisted' to the start of the block? Because that's how every other language does it. Because it's cheap and dead simple to reason about.
If it didn't, that would mean you could have multiple variables with the same name in the same block. That would be more difficult to keep track of, both for the compiler/runtime and for the programmer. It would also be largely pointless, because once execution gets to the code past the second variable declaration, you can't reference the variable created by the first declaration (unless you capture it with a closure).
function example() {
let a = false;
console.log(a);
let a = 10; // Past this point, I can't get to the first a anymore
console.log(a);
}
The TDZ addresses the design decision of being able to reference a variable before it's declared.
function example() {
console.log(a); // Without a TDZ, this will print undefined. With TDZ, this will be an error.
let a = 10;
}
If you google around for examples of 'hoisting' gone bad and run through what would have happened if vars had TDZ, you'll see that all of them would be avoided.
Why is it useful to know both about block scoping and TDZ?
Block scoping lets you know that:
function example(b) {
let a =10;
if (b) {
let a = 20;
console.log(a); // This will print 20, because it refers to the variable in the if block
}
console.log(a); // While this will print 10, because it refers to the variable in the function block
}
While TDZ lets you know that:
function example() {
console.log(a); // This will raise an error
let a = 10;
}
- peeters 11y agoThanks for the reply...I definitely get the advantage and popularity of lexical scoping. I'm not sure about your claim about every language doing hoisting though; take Java as an example: class Example { String f = "foo"; void foo() { System.out.println(f); //foo { System.out.println(f); //foo String f = "bar"; System.out.println(f); //bar } System.out.println(f); //foo String f = "baz"; System.out.println(f); //baz } } I understand that Java doesn't perfectly match var or let, in that the lexical matching of local variables is not the same as either. But Java really doesn't seem to be doing hoisting here, and it's certainly clearer what f refers to before the local variable is declared (though I like the explicit error the TDZ gives the best).
- ajanuary 11y agoJava defines the scope of a local variable to be "the rest of the block in which the declaration appears". The variable declaration is 'hoisted' to the top of the scope, which is defined as where the variable declaration is. Tautological! The rest of your example then falls out the fact local variables can shadow member variables [1]. C# scopes local variables more similarly to javascript's "let". class Example { public void Foo() { Console.WriteLine(f); // Error: The name 'f' does not exist in the current context } public void Bar() { Console.WriteLine(f); // Error: Cannot use local variable 'f' before it is declared String f = "a"; } } In Bar, it knows about the variable f, evidenced by the fact the error message is different to Foo. In Javascript terminology, f was 'hoisted' to the top of Bar. In regular terminology, f is in the lexical scope created by Bar. [1] Local variables can only shadow member variables, not other local variables. The justification the specification gives for shadowing is so that superclasses can introduce protected member variables without forcing subclasses to rename their local variables. You can still reference the member variable using "this.memberVariable".