Shadowing
“Shadowing” might sound mysterious and a little bit sketchy. But don’t worry, it’s completely legit!
Our running example for these chapters uses different variable names across the scope boundaries. Since they all have unique names, in a way it wouldn’t matter if all of them were just stored in one bucket (like RED(1)).
Where having different lexical scope buckets starts to matter more is when you have two or more variables, each in different scopes, with the same lexical names. A single scope cannot have two or more variables with the same name; such multiple references would be assumed as just one variable.
So if you need to maintain two or more variables of the same name, you must use separate (often nested) scopes. And in that case, it’s very relevant how the different scope buckets are laid out.
Consider:
var studentName = "Suzy";
function printStudent(studentName) {
studentName = studentName.toUpperCase();
console.log(studentName);
}
printStudent("Frank");
// FRANK
printStudent(studentName);
// SUZY
console.log(studentName);
// Suzy
TIP: |
---|
Before you move on, take some time to analyze this code using the various techniques/metaphors we’ve covered in the book. In particular, make sure to identify the marble/bubble colors in this snippet. It’s good practice! |
The studentName
variable on line 1 (the var studentName = ..
statement) creates a RED(1) marble. The same named variable is declared as a BLUE(2) marble on line 3, the parameter in the printStudent(..)
function definition.
What color marble will studentName
be in the studentName = studentName.toUpperCase()
assignment statement and the console.log(studentName)
statement? All three studentName
references will be BLUE(2).
With the conceptual notion of the “lookup,” we asserted that it starts with the current scope and works its way outward/upward, stopping as soon as a matching variable is found. The BLUE(2) studentName
is found right away. The RED(1) studentName
is never even considered.
This is a key aspect of lexical scope behavior, called shadowing. The BLUE(2) studentName
variable (parameter) shadows the RED(1) studentName
. So, the parameter is shadowing the (shadowed) global variable. Repeat that sentence to yourself a few times to make sure you have the terminology straight!
That’s why the re-assignment of studentName
affects only the inner (parameter) variable: the BLUE(2) studentName
, not the global RED(1) studentName
.
When you choose to shadow a variable from an outer scope, one direct impact is that from that scope inward/downward (through any nested scopes) it’s now impossible for any marble to be colored as the shadowed variable—(RED(1), in this case). In other words, any studentName
identifier reference will correspond to that parameter variable, never the global studentName
variable. It’s lexically impossible to reference the global studentName
anywhere inside of the printStudent(..)
function (or from any nested scopes).
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 behavior 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 (RED(1)), 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
. That’s not entirely accurate, but it’s good enough for our discussion. In the next chapter, we’ll explore the global scope/object topic more.
Consider this program, specifically executed as a standalone .js file in a browser environment:
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
(which we’re pretending for now is synonymous with the global object). That’s the only way to access a shadowed variable from inside a scope where the shadowing variable is present.
The window.studentName
is a mirror of the global studentName
variable, not a separate snapshot copy. Changes to one are still seen from the other, in either direction. You can think of window.studentName
as a getter/setter that accesses the actual studentName
variable. As a matter of fact, you can even add a variable to the global scope by creating/setting a property on the global object.
WARNING: |
---|
Remember: just because you can doesn’t mean you should. Don’t shadow a global variable that you need to access, and conversely, avoid using this trick to access a global variable that you’ve shadowed. And definitely don’t confuse readers of your code by creating global variables as window properties instead of with formal declarations! |
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
.
Other forms of global scope declarations do not create mirrored global object properties:
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:
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 RED(1) special
is shadowed by the BLUE(2) special
(parameter), and the BLUE(2) special
is itself shadowed by the GREEN(3) special
inside keepLooking()
. We can still access the RED(1) special
using the indirect reference window.special
. But there’s no way for keepLooking()
to access the BLUE(2) special
that holds the number 112358132134
.
Copying Is Not Accessing
I’ve been asked the following “But what about…?” question dozens of times. Consider:
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 another
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 BLUE(2) special
parameter to a different value from inside keepLooking()
.
Another “But…!?” you may be about to raise: what if I’d used objects or arrays as the values instead of the numbers (112358132134
, etc.)? Would us having references to objects instead of copies of primitive values “fix” the inaccessibility?
No. Mutating the contents of the object value via a reference copy is not the same thing as lexically accessing the variable itself. We still can’t reassign the BLUE(2) special
parameter.
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
// ..
}
}
function another() {
// ..
{
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 syntax error description in this case indicates that special
has already been defined, but that error message is a little misleading—again, no such error happens in something()
, as shadowing is generally allowed just fine.
The real reason it’s raised as a SyntaxError
is because the var
is basically 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, so this variant raises no exception:
function another() {
// ..
{
let special = "JavaScript";
ajax("https://some.url",function callback(){
// totally fine shadowing
var special = "JavaScript";
// ..
});
}
}
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.