Shadowing in JavaScript: What You Need to Know
What is Shadowing and How Does it Affect Your JavaScript Code?
What is shadowing
“Shadowing” might sound mysterious and a little bit sketchy.
But don’t worry, it’s completely legit!
Consider the following code:
var studentName = "Suzy"; //global scope
function printStudent(studentName) {
//local scope of function printStudent
studentName = studentName.toUpperCase();
console.log(studentName);
}
printStudent("Frank");
// FRANK
printStudent(studentName);
// SUZY
console.log(studentName);
// Suzy
If you notice in the above code inside the printStudent
we have a statement:
studentName = studentName.toUpperCase();
There are two declarations of the variable studentName
one in the global scope and the other in function. So what do you think this statement will refer to which declaration of this variable? To refer to this your JS engine will first check that is this variable student name already present in the current scope where this variable is referred, you can see that it is already available in this scope so will will take the value which is assigned to it here: ’Frank’.This is a key aspect of lexical scope behavior, called shadowing. The printStudent
function’s studentName
variable (parameter) shadows the global scope studentName
. So, the parameter is shadowing the (shadowed) global variable.
Global Unshadowing Trick:
Please beware: leveraging the technique I’m about to describe is not very good practice, as it’s limited in utility, confusing for readers of your code, and likely to invite bugs to your program. I’m covering it only because you may run across this behaviour in existing programs, and understanding what’s happening is critical to not getting tripped up. It is possible to access a global variable from a scope where that variable has been shadowed, but not through a typical lexical identifier reference.
In the global scope var declarations and function declarations also expose themselves as properties (of the same name as the identifier) on the global object—essentially an object representation of the global scope.
If you’ve written JS for a browser environment, you probably recognize the global object as window
.
consider this code:
var studentName = "Suzy";
function printStudent(studentName) {
console.log(studentName);
console.log(window.studentName);
}
printStudent("Frank");
// "Frank"
// "Suzy
Notice the window.studentName
reference? This expression is accessing the global variable studentName
as a property on window
.
This little “trick” only works for accessing a global scope
variable (not a shadowed variable from a nested scope), and even then, only one that was declared with var or function.
var one = 1;
let notOne = 2;
const notTwo = 3;
class notThree {}
console.log(window.one); // 1
console.log(window.notOne); // undefined
console.log(window.notTwo); // undefined
console.log(window.notThree); // undefined
Variables (no matter how they’re declared!) that exist in any other scope than the global scope are completely inaccessible from a scope where they’ve been shadowed:
Consider this code :
var special = 42;
function lookingFor(special) {
// The identifier `special` (parameter) in this
// scope is shadowed inside keepLooking(), and
// is thus inaccessible from that scope.
function keepLooking() {
var special = 3.141592;
console.log(special);
console.log(window.special);
}
keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 42
The global special is shadowed by the
special (parameter), and the parameter special is itself
shadowed by the special inside keepLooking().
We can still access the global special using the indirect reference window.special
But there’s no way for keepLooking()
to access the parameter special that holds the number 112358132134.
Copying Is Not Accessing
What about we store this parameter as a property of the object and then try to access it? See below code :
var special = 42;
function lookingFor(special) {
var another = {
special: special
};
function keepLooking() {
var special = 3.141592;
console.log(special);
console.log(another.special); // Ooo, tricky!
console.log(window.special);
}
keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 112358132134
// 42
Oh! So does this object technique disprove my claim that the special
parameter is “completely inaccessible” from inside keepLooking()
? No, the claim is still correct. special: special
is copying the value of the special parameter variable into another container (a property of the same name). Of course, if you put a value in another container, shadowing no longer applies (unless another was shadowed, too!). But that doesn’t mean we’re accessing the parameter special; it means we’re accessing the copy of the value it had at that moment, by way of another container (object property).
We cannot reassign the special parameter to a different value from inside keepLooking().
Illegal Shadowing
Not all combinations of declaration shadowing are allowed. let can shadow var, but var cannot shadow let:
function something() {
var special = "JavaScript";
{
let special = 42; // totally fine shadowing
// ..
}
}
{
// ..
{
let special = "JavaScript";
{
var special = "JavaScript";
// ^^^ Syntax Error
// ..
}
}
}
Notice in the another()
function, the inner var special
declaration is attempting to declare a function-wide special, which in and of itself is fine (as shown by the something()
function).
The real reason it’s raised as a SyntaxError is because the
var is trying to “cross the boundary” of (or hop over)the let declaration of the same name, which is not allowed. That boundary-crossing prohibition effectively stops at each function boundary.
Summary: let (in an inner scope) can always shadow an outer scope’s var. var (in an inner scope) can only shadow an outer scope’s let if there is a function boundary in between.