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:

  1. var studentName = "Suzy";
  2. function printStudent(studentName) {
  3. studentName = studentName.toUpperCase();
  4. console.log(studentName);
  5. }
  6. printStudent("Frank");
  7. // FRANK
  8. printStudent(studentName);
  9. // SUZY
  10. console.log(studentName);
  11. // 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:

  1. var studentName = "Suzy";
  2. function printStudent(studentName) {
  3. console.log(studentName);
  4. console.log(window.studentName);
  5. }
  6. printStudent("Frank");
  7. // "Frank"
  8. // "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:

  1. var one = 1;
  2. let notOne = 2;
  3. const notTwo = 3;
  4. class notThree {}
  5. console.log(window.one); // 1
  6. console.log(window.notOne); // undefined
  7. console.log(window.notTwo); // undefined
  8. 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:

  1. var special = 42;
  2. function lookingFor(special) {
  3. // The identifier `special` (parameter) in this
  4. // scope is shadowed inside keepLooking(), and
  5. // is thus inaccessible from that scope.
  6. function keepLooking() {
  7. var special = 3.141592;
  8. console.log(special);
  9. console.log(window.special);
  10. }
  11. keepLooking();
  12. }
  13. lookingFor(112358132134);
  14. // 3.141592
  15. // 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:

  1. var special = 42;
  2. function lookingFor(special) {
  3. var another = {
  4. special: special
  5. };
  6. function keepLooking() {
  7. var special = 3.141592;
  8. console.log(special);
  9. console.log(another.special); // Ooo, tricky!
  10. console.log(window.special);
  11. }
  12. keepLooking();
  13. }
  14. lookingFor(112358132134);
  15. // 3.141592
  16. // 112358132134
  17. // 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:

  1. function something() {
  2. var special = "JavaScript";
  3. {
  4. let special = 42; // totally fine shadowing
  5. // ..
  6. }
  7. }
  8. function another() {
  9. // ..
  10. {
  11. let special = "JavaScript";
  12. {
  13. var special = "JavaScript";
  14. // ^^^ Syntax Error
  15. // ..
  16. }
  17. }
  18. }

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:

  1. function another() {
  2. // ..
  3. {
  4. let special = "JavaScript";
  5. ajax("https://some.url",function callback(){
  6. // totally fine shadowing
  7. var special = "JavaScript";
  8. // ..
  9. });
  10. }
  11. }

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.