The Goal: Understanding the difference between lexical and block scoping
Motivation: These are subtle differences that newer devs (like myself) may not know or be aware of since we've never really used the
var
keywordLet's start with an example using the below code sample.
function logTenOrFifteen() {
if (true) {
let x = 10
processVar(x)
} else {
let x = 15;
processVar(x)
}
}
function processVar(param) {
console.log(param);
}
logTenOrFifteen();
Like every good developer, your instinct cries out to remove the duplication. We should only call
processVar
once. While having it twice might not be the end of the world, imagine there are more functions that need to be called while passing in x
. You'd end up with a lot of duplication. Not very DRY of you. Being a clever engineer, you refactor the above block to
function logTenOrFifteen() {
let x;
if (true) {
x = 10;
} else {
x = 15;
}
processVar(x)
}
function processVar(param) {
console.log(param);
}
logTenOrFifteen();
With this clean implementation you give yourself a pat on the back and a 🌟. Now there's only one call to
processVar
Hooray! Good software engineering wins the day! Except, you might have a little voice in the back of your head nagging you about the
x
variable declaration being separate from its assignment. This can especially be a problem if the let x;
is several lines above the if/else
statement. This is where the difference between block scoping and lexical scoping matters. Since we're using
let
, x
is block scoped. We can't achieve the following two goals when using block scoped variables. processVar
if
and else
blocksFor #2 we'd ideally like to move the
let x;
that appears outside of the if/else
into it. Doing so however would require calling processVar
in both parts of the conditional violating #1. Block scoped variables only exist through the use oforlet
when variables are declared.const
Quite the conundrum we have...
If block scoping is the problem, how would lexical scoping help? Lexical scoping (using the
var
keyword) would let us achieve both #1 and #2.Here's how
function logTenOrFifteen() {
if (true) {
var x = 10
} else {
x = 15;
}
processVar(x)
}
function processVar(param) {
console.log(param);
}
logTenOrFifteen();
Now we have the variable initialization limited to just the
if/else
block AND a single call to processVar
. How exciting! We got the best of both worlds!This solution works due to how javascript processes the function and how it hoists, initializes, and allows for the redefining of variables that are lexically scoped.
When we use the example above, the declaration of
x
is hoisted to the top of the function and since its lexically scoped, is also assigned a default value of undefined
. The remaining assignment to 10
or 15
is left in the if/else
block. The example above gets transformed by the javascript compiler tofunction logTenOrFifteen() {
var x;
if (true) {
x = 10
} else {
x = 15;
}
processVar(x)
}
function processVar(param) {
console.log(param);
}
logTenOrFifteen();
This is the same as our initial refactor where we moved the
let
outside of the if/else
block and then assign values to the variable inside them. The only difference is that to achieve this effect we had to use var
(lexical scoping) instead of let
(block scoping). Javascript just gives some nice sugar for lexically scoped variables. This is something you should NOT do. For one, the end result is the same. Except now since we used
var
, the variable is accessible outside of the block (which makes sense, variables declared with var
are lexically scoped). This can lead to many issues that required the introduction of block scoped variables. The same difference between lexically and block scoped variables cause the following odd behaviors.function foo() {
let x = 10;
if (true) {
let x = 12;
console.log(x); // outputs 12;
}
console.log(x); // outputs 10
}
function foo() {
var x = 10;
if (true) {
var x = 12;
console.log(x); // outputs 12
}
console.log(x); // outputs 12
}
The first
foo
function acts as expected. We wouldn't expect the second console log to print 12 since we'd expect that the value for x
of 12 should only apply in the if statement and not outside. If it were simply a reassignment (x = 12
) then we'd expect the output of both logs to be 12.The second
foo
function however prints 12 twice even though the variable is redeclared. This is due to the fact that in this case, the variable is lexically scoped since the variable was created with var
. Lastly given this example
function foo() {
let x = 5;
let x = 10;
}
function foo() {
var x = 5;
var x = 10;
}
the first function will throw an error that x cannot be redeclared. In contrast, the second
foo
function works. This is another key difference between lexical and block scoped variables. Lexical variables can be redeclared whereas block scoped variables cannot. To fix the first function, you'd have to remove the let
in the second assignment. I imagine javascript compiles the second foo function to something like function foo() {
var x;
x = 5;
x = 10;
}
since its lexically scoped.
undefiend
making them accessible before they're defined. Ex: console.log(foo); var foo = 'hello'; // This would log undefined
ReferenceError
. Ex: console.log(foo); let foo = 12; // This would throw an error
This was quite a deep dive but I hope it helped. I recommend reading Will Vincent's article for more insight into some of these differences.