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.
- Will return
Tibbers is alive
because it’s set as a global variable at line 2. - Will return
Tibbers is safe
because a new object has been created. The original object at line 1 remains unchanged. - Will return
Tibbers is safe
because a new object has been created and attached to thefirstJump()
function. - Will return
Tibbers is alive
becausethis
refers to the object that is immediately outside the nearest {}, aka— the parent scope in the call stack — which in this case is the globalwindow
object. - Will return
Tibbers is dead
because it is referring directly to the object declared inside {} - Will return
Tibbers is dead
because it is referring directly to the object declared inside {} - Will return
Tibbers is flying
and permanently modifies the variable declared on line 6. - Will return
Tibbers is meowing
and permanently modifies the variables declared on line 13 while line 5 remains unchanged. - 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. - Will return
Tibbers is flying
because it was permanently modified throughthis
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.