How Variable Declarations Can Impact Your Scope In JavaScript

Like anything else we love—cats for example—it's easy to take JavaScript for granted. One good example where many are complacent is let and var, because they seem to both work the same way — until they don’t.

Some people say to just use let because it’s “better,’ but that doesn’t really tell us much about anything in particular. That’s because JavaScript scope can be a big and funky topic that no one really wants to properly tackle.

So for today, we’re going to figure out if a kitty named Tibbers gets to live or die — based on the chosen variable type declaration. This is because in JavaScript, let and var isn’t a Schrodinger’s cat scenario, and the status is already pre-determined based on what you used. [cue sinister music]

The Secret Life of “this”

When writing functions, we try to avoid dependency injections as much as we can for cohesion purposes. The last thing we want is a set of functionalities that are interdependent with each other. Declared variables, therefore, become the go-to for storing permeable data.

Accessing this data in JavaScript is often done through the keyword this.

But this is a contentiously misunderstood keyword with impacts on the code based on how it is consumed and where it sits relative to the call. Sometimes, this feels like it has a life of its own, and in a way, it does.

The default scope of this sits at the window-object level, making it a global object that is accessible by everything that calls this against a particular declared piece of code.

For example:

var cat = 'Tibbers';
console.log(this.cat); //returns Tibbers
console.log(window.cat); // also returns Tibbers 
console.log(cat); // say hello to Tibbers again.

While it may seem simple, things start to get funky when you throw in let.

Using let scopes your variable to the nearest enclosing block while var is scoped to the nearest function block. It’s not attached to the window object and is its own object.

Let’s take a look at the example below:

let cat = 'Tibbers';
console.log(this.cat); //will return undefined
console.log(cat); //will return Tibbers because cat is its own object

While var runs a free-for-all kind of gig by attaching itself to the window object (which is synonymous with this keyword), let limits the scope and ring-fences the block, statement, or expression where it is declared and used.

In the example above, let cat is set at the root level and exists as its own object.

“Enclosing Function Block” What?

JavaScript is a lexical language by design. This means that inheritance flows inwards and a variable outside the function is available for usage inside, but not the other way around.

Closures in JavaScript are part of a process for permeating data beyond what’s occurred within the function.

When you use let, it’s scoped to the closest closing block. In short, if you see {}, it’s a block. A specific declared let only exists within the corresponding block.

If it’s declared at the root level, the variable is scoped as a global object and accessible by everything. If it’s declared inside a function, it is scoped to that closing block only — a sort of private variable without being explicitly called private.

This comes in handy for when you want to do something inside your function but don’t want it to impact on the values that may have the same name outside. It becomes hidden and does not exist beyond the pair of {} it sits in.

function cats(){
  if(true){
    var firstCat = "Tibbers";
    let secondCat = "Not Tibbers";
    const thirdCat = "Friend of Tibbers";
  }
  
  console.log(firstCat); //will log because var is scoped to the function block.
  console.log(secondCat); //will throw error because it's not in the same or inner block. 
  console.log(thirdCat);
}
cats();

The following code is not the best representation for best practices but demonstrates let-block scoping and inward inheritance.

function cats(){
  if(true){
    var firstCat = "Tibbers";
    let secondCat = "Not Tibbers";
    const thirdCat = "Friend of Tibbers";
    
    if(true){
      //will log because still within enclosing block and inheritance flows inwards
      console.log(secondCat); 
      console.log(thirdCat);  
    }
    
  }
}
cats();

What About Const?

Sometimes, you just need a piece of data to remain constant throughout your code. This means that you can’t change the value once it’s set, even if you tried. If you really want to change it, you’ll need to go and physically do it at the source.

That’s essentially what const is.

const behaves like let with the caveat that you can’t change it once it’s declared. This comes in handy for things that are unlikely to change like tax values, conversion units, and the maximum number of item types in a cart.

If you try to change a const value, your console will throw an error that looks something like this:

Will Tibbers Live? Die? Or Fly?

When we put it all together, using var and let in specific places can result in a different outcome for a specific variable. While most cases won’t be as dramatic as determining if a cat will live or die, it can also affect adding to an item to a cart correctly or if complex tax rules are calculated properly.

So, let's look at the code below and we’ll walk through it together. Please note that nesting is not the recommended method for writing code but only used here for scoping demonstration purposes.

let jumpOne = "is alive";
var jumpTwo = "is alive";

function firstJump(){
  let jumpOne = "is safe";
  var jumpTwo = "is safe";
  
  console.log("Tibbers " + jumpOne); // 3
  console.log("Tibbers " + jumpTwo); // 4
  console.log("Tibbers " + this.jumpTwo); // 5
  
  if(true){
    let jumpOne = "is dead";
    var jumpTwo = "is dead";
    this.jumpTwo = "is flying";
    
    console.log("Tibbers " + jumpOne); // 6
    console.log("Tibbers " + jumpTwo); // 7
    console.log("Tibbers " + this.jumpTwo); // 8
    
    if(true){
      jumpOne = "is meowing";
    }
    
    console.log("Tibbers " + jumpOne); // 9
  }
}

  console.log("Tibbers " + jumpOne); // 1
  console.log("Tibbers " + jumpTwo); // 2
  firstJump();
  console.log("Tibbers " + jumpOne); // 10
  console.log("Tibbers " + jumpTwo); // 11

Will return Tibbers is alive because it’s set as a global variable at line 1.

  1. Will return Tibbers is alive because it’s set as a global variable at line 2.
  2. Will return Tibbers is safe because a new object has been created. The original object at line 1 remains unchanged.
  3. Will return Tibbers is safe because a new object has been created and attached to the firstJump() function.
  4. Will return Tibbers is alive because this refers to the object that is immediately outside the nearest {}, aka— the parent scope in the call stack — which in this case is the global window object.
  5. Will return Tibbers is dead because it is referring directly to the object declared inside {}
  6. Will return Tibbers is dead because it is referring directly to the object declared inside {}
  7. Will return Tibbers is flying and permanently modifies the variable declared on line 6.
  8. Will return Tibbers is meowing and permanently modifies the variables declared on line 13 while line 5 remains unchanged.
  9. Will return Tibbers is alive because all variables declared inside the function are their own separate variables or are just updating to the nearest block scope.
  10. Will return Tibbers is flying because it was permanently modified through this keyword, which references the window object.

Final Words

The whole var and let thing can be unnecessary complicated, especially when the same variable names are used. But it does happen in the wild and a lot more than anyone would like.

If this is still slightly confusing, the easiest way to remember it is that let accessibility is limited to the {}it exists in while var is accessible within the function(){} block.

Changes to let value are scoped to the nearest declared let value.

If variables are declared at the global level, this keyword is the way to access var and the variable name itself for let variables.