Understanding scope in Javascript is tricky and is often a source of bugs in JavaScript for someone new to the language. I’ll attempt to boil down the concept here so that it is better understood moving forward.
To “scope” my teachings here, I will use and talk about JavaScript that follows ES7+ (ECMA 2017+) as I feel the standard has pushed JavaScript a long way from where it began. We should be focusing on learning the more modern and capable syntax and features of the language.
It is important to note that older browsers like IE11 do not support ES6+, so to support this older, more ancient browser, you must “polyfill” the functionality or use Babel to transpile your modern JavaScript ES6+ features down to ES5.
Transpiling is similar to compiling. Transpiling is morphing the language into a subset language, except in this case, an older version of it.
What are Javascript scopes?
To trivialize the information a bit, there are two significant types of scope to consider. Global scope and local scope. (By local scope, I include block/function scope, let’s think of them as the same.) Let’s take the following example:
var i = 5;
const coolFunction = () => {
var i = 10;
console.log(i);
let x = 77;
}
coolFunction();
console.log(i);
console.log(x);
What would you expect i and x to be here? What scope is coolFunction
and i
? Let’s explore here.
var i = 5; // <-- i is global scope
const coolFunction = () => { <-- coolFunction is global scope
var i = 10; <-- is redeclared here as a local variable, however we have a collision with the global scope!
let x = 77; <-- x is a local scope (local to coolFunction)
}
The output is:
10
5
exception!
To answer the remaining questions from earlier, i
is 5 and x
is undefined (we will get an exception!)
Note that x here is undefined when we try to access it. Why? In a nutshell, x
is not exposed outside of the coolFunction
, which means the scope encapsulates it. Further x is declared when we call coolFunction()
, and then the function ends, the scope is over, and the resources are freed; thus no longer accessible.
Using const and let over var
Also, I use var in some places, as a rule of thumb, always use let or preferably const (const means the variable ‘pointer’ will be immutable and cannot be re-assigned). Use const first. Otherwise, if it needs to be changed, then use let.
Since this is on the side and easy to remember (use this, not that), I won’t go into great detail about why use const and let over var. In a nutshell, primarily, it utilizes critical block scope features introduced ES6 that will help with dealing ‘hoist’ headaches where var declared variables would be hoisted to the top of the scope just outside of their declared scope.
Bringing it all together
We can clean the code up and correctly use the scope as so:
const i = 5;
const coolFunction = () => {
const i = 10;
console.log(i);
const x = 77;
return x;
}
const x = coolFunction();
console.log(i);
console.log(x);
output:
10
5
77
This is entirely valid, but I’d say it’s still poor taste to use the same variable name as the outer scope for i. The variable with i = 10
would is in the coolFunction scope and freed after it ends. However, it is not the most readable and can be confusing without looking closely. Best to rename the inner i
and use a better variable name. (Yes, we can remove it since it’s unused, but I am keeping it for the example).
const i = 5;
const coolFunction = () => {
const p = 10;
console.log(i);
const x = 77;
return x;
}
const x = coolFunction();
console.log(i);
console.log(x);
output:
5
5
77
We see the console.log() printing out 5 twice; this is because variables declared in scopes directly above the block scope are accessible from the sub-scope.
Avoid polluting the global scope
Further to go back to global scope, both i
and coolFunction
are declared globally. Generally, declaring things on the global scope should be avoided unless you explicitly intend for it for some advanced reason. Avoid it by wrapping all of your scopes within another scope. Of course, at some level, you will be left with still one function at the global level that encapsulates your entire program. We can do this with the example as so:
(function() {
const i = 5;
const coolFunction = () => {
const p = 10;
console.log(i);
const x = 77;
return x;
}
const x = coolFunction();
console.log(i);
console.log(x);
})();
This self-invoking closure creates a function block scope that runs my full app in no go. There are no variables here; the global scope is as clean as what the browser gives off at the start. The function scope will run through, and all resources are freed afterward.
Closures
To wrap up this subject, I also want to explain what closures are because they are closely related to this topic. Closures are more or less functions with their scope created and returned from the scope of another function. Let’s see this example to understand what this means:
const coolFunction = () => {
const x = 5;
return () => {
return x;
};
};
const innerFunc = coolFunction();
console.log(innerFunc());
outputs 5
This piece is creating a function that returns a closure that exposes the inner variable of coolFunction that would not usually be exposed. When the closure is called, it returns the value of ‘x,’ which the closure has access to as it is still in the sub-scope of coolFunction even though it is being called in a different scope. Closures can almost be thought of as an “object” that encapsulates a single state captured at the time it was created.
With the understanding of scope and closures, you should have a firm grasp of some of the more complex JavaScript constructs that generally throw developers into a whirl. Failure to master these “gotchas” in JavaScript would inevitably lead to poor decisions and bugs.
Did I explain the process of understanding JavaScript scope well? Let me know in the comments so I can refine this article and improve my writings. Also, check out other articles I have or will write similar to this subject here.