Scoping in JavaScript: The Basics by@sambernheim

Scoping in JavaScript: The Basics

image
Sam Bernheim HackerNoon profile picture

Sam Bernheim

Software Engineer @ Twitter

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

Signup or Login to Join the Discussion

Tags

Related Stories