JS uses lexical scoping, meaning that a scope is defined as part of the nested structure of the code. Anything referencing the name of a variable anywhere in that scope can attempt to access the value. What the actual value is depends on when the value is initialized, which is a runtime property of the code.
When people talk about hoisting
in JS generally it is not super clear which of these things they are describing. In JS, because scopes are lexically-based, the existence of a given variable is hoisted, in that it always exists within that scope.
The actual initialization of a variable in a scope depends on the way the variable is declared.
function foo(){}
declarations have their initialization hoisted, so the variable foo
will always be available in the body of the function, even if the declaration comes later. e.g.
foo();
function foo(){}
is perfectly valid valid.
var foo = 4;
hoists initialization, but the value is initialized to undefined
until the declaration has executed, e.g.
foo; // undefined
var foo = 4;
foo; // 4
let
/const
performs initialization only when executed, and attempts to access uninitialized variables will throw an exception.
foo; // would throw because it is not initialized yet
let foo = 4;
foo; // 4
So taking those together, the behavior of variables is only determined at runtime. For your code example, it depends on when the function is called. Since let foo = 4;
only initialized foo
when it runs, if the function were called first, it would fail, and if the function were called after, it would work.
function log(){
console.log(foo);
}
log(); // would throw because foo has not been initialized
let foo = 4;
log(); // would log 4
This behavior of delayed initialization for let
and const
is commonly referred to as the "temporal dead zone".
At the end of the day, as long as the variable has been initialized and has the value you care about by the time you access it, your code is fine. Whether you have access to the variable in the first place only depends on the nesting of your code.