Scoping in JavaScript: The Basics

Author profile picture

@sbernheimSam Bernheim

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
keyword
Let'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.
  1. A single call to
    processVar
  2. Keeping the variable assignment only in the
    if
    and
    else
    blocks
For #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 of
let
or
const
when variables are declared.
Quite the conundrum we have...

A (bad) Solution

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 to
function 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.

Odd Behaviors

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.

Key Takeaways

  1. Block scoped variables cannot be redeclared to a new value in the same scope as one another
  2. Lexically scoped variables can be redeclared to a new value in the same scope as one another
  3. Both lexically and block scoped variables are hoisted,
  4. Lexically scoped variables, when hoisted, are given a default value of
    undefiend
    making them accessible before they're defined. Ex:
    console.log(foo); var foo = 'hello'; // This would log undefined
  5. Block scoped variables, when accessed before assignment, throw a
    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.

Comments

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!