Block Scope and Hoisting – An Exercise in Rubber Ducking

This is a follow up to  ‘Intro to Scope’, previously posted here at CrookedCode.  It’s part of our rubber ducking series, where we’re attempting to work through some of the more difficult parts of JavaScript using Kyle Simpson’s book series, You Don’t Know JS, as a guide.

If you want to know more about the project, check out this post.  If you’re thinking to yourself “Where does this guy get off thinking he can explain this stuff better than Kyle Simpson?”, check out the disclaimer here. Otherwise, read on…

As the title suggests, this post will cover block scope versus function scope, particularly as they pertains to varlet and const. I’d like to start with the concept of hoisting, however, as I think it’ll help understand the rest of the article.

WTF is Hoisting?  (And Do I need a boat to do it?)

Consider the following:

The question here is will “Will this get logged?” actually get logged to the console even though hoisted is declared after it  A) has a value assigned to it and B) gets logged.

The answer is yes, “Will this get logged?” is logged to the console.

You’ve also probably seen examples of functions getting invoked before they’re even declared.  Have you wondered why that works too…

Something like:

The above snippet will log ‘Henry’ to the console.

Both of the above examples work because of hoisting.  As we mentioned in the previous article, JavaScript is a compiled language.  Part of the compilation step is finding all the declarations (variables and functions) and associate them with the appropriate scope.

This part actually occurs first, before the code is executed.  Therefore any function or variable declared in a given scope will be available throughout that scope.

In the first example above, var hoisted would have been ‘hoisted’ to the top of scope within the hoist() function.  So when the JS engine comes across hoisted = a and searches for a variable called hoisted in the lexical scope within the hoist() function, it’ll find it!

One more note on hoisting…

Don’t confuse declaring a variable with assigning a value to a variable, even if that happens in the same line of code.

var name = 'Henry'; is seen as two statements by the JS engine, var name; and name = 'Henry';. The declaration is hoisted, the assignment is not…

If you think this is just semantics, consider the following:

Someone with a tenuous grasp on scope might look at this snippet of code and think there are a couple possibilities…

“That’ll probably be a ReferenceError because a is declared after it’s logged..”

Nope…

or if they’ve heard of hoisting….

“Maybe it’ll print ‘Henry’ to the console because a is hoisted within the printYourDogsName function”

Nope again…

The actual answer is that the snippet logs undefined.  If you think back to the last article, undefined (as opposed to ReferenceError) means that the JS engine found the variable a (because it was hoisted), but a was undefined (because the a = name assignment didn’t happen until after console.log(a).

Ok, enough about hoisting… I think that should give us a pretty good grasp on how things work.  For more detail please refer to YDKJS.

The Complexity of Scope

I hope my previous article, along with what’s covered here, has given you a pretty good understanding of lexical scope and how it works in JavaScript.  So why did Kyle Simpson feel the need to devote an entire book to scope and closures?

You know the old saying ‘You don’t know what you don’t know…”?  Well, there are several special circumstances that are covered in ‘Scope and Closures’ that I won’t be addressing here at Crooked Code of which you should be aware.

One such concept is using scope to hide access to variables.  I plan to touch upon some of this in a future article on Modules, but in the meantime, YDKJS covers it here.

You can also read about the specifics of scope as it pertains to function expressions, including Immediately Invoked Function Expressions or IIFEs, here.

The final topic you should be aware of that I won’t be covering in this article is closure.  Simpson devotes an entire chapter to closure and I highly recommend reading it.

I’ve read it a couple times and my pulse still quickens a bit when someone mentions closure (I’ll be writing a rubber ducking article here on closure in the near future to solidify the concept in my own head).

Now on to Block Scope

We’ve discussed previously that JavaScript uses functions to define scope.

This was, for the most part, entirely true until ES6.  Among the changes in ES6 were the keywords let and const, which provide us ways to declare variables with block scope.

The only difference between the two is const declares a constant, which can’t be changed once assigned a value.

Before we get into what block scope is, there’s a key note to remember when dealing with let and const.  They are NOT hoisted within their block of scope.  This means that you must declare them before you use them or it will result in a ReferenceError.

Now, what’s block scope?  Simpson tells us a block is commonly a {...} pair. So think for loops, if statements, etc..  You can even create your own block of scope by enclosing code in curly brackets.

Block scope can be useful in several key ways…  Consider the following:

In this example, as I’m sure you can figure out by now, 10 (not 42) would get logged to the console.  This is because i would be hoisted to the scope within the redundantI function.  So when the engine comes across the var i = 0 statement in the for loop, it will already see the i variable and use that one.  Now consider:

This snippet would actually result in 42 getting logged.  Why? Because let created a new i variable for use ONLY in the for loop!

I realize this example is pretty idiotic and you’re probably thinking to yourself “This guy is effing nuts… I’d never make that mistake in my code…”.

I know you wouldn’t make a mistake like that, but think about functions that are 100+ lines of code with a couple additional functions nested within.

If you’re using a variable for the sole purpose of controlling the for loop, wouldn’t you prefer to keep it out of the overall scope? Wouldn’t it be nice if you could use the variable in the loop, then have it go away?  let enables that…

Simpson also discussed using block scope to enable garbage collecting when you’re handling large amounts of data.  It’s a pretty interesting concept and can be read in the garbage collection section of Chapter 3.

The final point to mention regarding the use of let concerns its use in loops.  Simpson points out that, not only does let bind the variable to the body of the for loop, but it re-binds the variable to each iteration of the loop.

This may seem trivial, but when you consider closure it can be a very useful behavior if you understand it enough to use it properly.  I’ll cover this in more detail in my article on closure, but you can read about it now in the YDKJS section called ‘Block Scoping Revisited’.

That’s it for now…  If anyone’s still reading, thanks for taking the time.  I hope I was able to straighten out the code for you.

-Jeremy

Intro to Scope – An Exercise in Rubber Ducking

As I stated previously, I’ll be writing a series of posts attempting to explain, or rubber duck, some of the more difficult topics in the You Don’t Know JS (YDKJS) series by Kyle Simpson.

Before you start screaming at me that Kyle Simpson has forgotten more about JavaScript than I ever knew, to which I would wholeheartedly agree, please read the following disclaimer…

Disclaimer:

I have great respect for Kyle Simpson, his method for teaching and the YDKJS series.  This series of posts were not endorsed by Simpson and are merely an attempt to supplement his books.  This project is being created for the sole purpose of helping to solidify these concepts in both your mind and mine.  If you are learning to code in JavaScript, I highly encourage you to read the YDKJS series.

What is Scope?

Variables are extremely important to computer programming.  They allow us to write complex and dynamic programs.

But how and where should we create variables? How and where do we store them?  And, maybe most importantly, how and from what part of the program are we able to access a particular variable?

Each language has very specific rules for these questions.  These rules can be thought of as Scope.  Here we’ll introduce the rules of scope specific to JavaScript.

Given that scope is a fairly broad topic, with many different aspects to cover, I think it best to break this into multiple blog posts.  The goal here will be to introduce the topic of scope and provide a foundation that we can build upon in later discussions.

What we cover here can be found in far greater depth in the 2nd YDKJS book – “Scope and Closures”.

Let’s begin by taking a step back…

JavaScript is a Compiled Language (surprise!!)

For those who have worked with more traditionally compiled languages like C++ or Java, it may come as a bit of a surprise that JavaScript is actually compiled before it runs.  This misunderstanding is likely because, unlike many other languages,  JavaScript is compiled just before run time rather than in an entirely different build step.

Right now you may be asking yourself why this is relevant to a discussion on Scope…  That’s a great question, let me try to explain.

Simpson describes that, at a very high level, the 3 basic steps in compilation are 1) tokenizing/lexing, 2) parsing and 3) code-generation.

The tokenizing or lexing step breaks up strings of characters in your code into chunks (tokens) that are easier to understand.  Next, the parsing step takes all these tokens and forms a tree of nested elements called an Abstract Syntax Tree. This tree represents the grammatical structure of the program.  It is from this AST that code is generated in step 3.

Still not clear on how this relates to scope, right?

To clear this up, you must understand that JavaScript employs lexical scoping rather than dynamic scoping.

This means that the scoping for a program is determined at the time of lexing, based on where the variables fall within the blocks of scope. Therefore the author is able to control the scope at the time he is writing the program without worrying that the scopes will change at run time.

This is a key concept in understanding the behavior of scope.  For more detail, please read the first 2 chapters in  “Scope and Closures”.

As with most rules, there are exceptions….  Simpson discusses several ways to change lexical scoping at run time.  This can be accomplished using eval() and with(), both of which he frowns upon using.  I don’t plan on going over these here, but you can be read about them in Ch. 2 .

General rules and behavior of scope in JavaScript

Generally speaking, (and again, there are several exceptions to this rule, which we will likely cover in later posts) JavaScript derives its scope from functions.

What does this mean?

You can think of scope as a series of boxes contained within each other. These boxes contain the functions and variables that are declared within that function.  This structure enables control over how one block of scope is allowed to access another.

In JavaScript, there is a scope (or box) that contains the entire program, called global scope.  Each time you create a new function, a new scope box is created.  This new scope will contain any new variables or function declared within (including any parameters declared in the function definition).

And so on, and so on, as functions continue to be nested within other functions…

Mentally, you can picture scope as something like this:

A group of nested boxes meant to depict the concept of scope.
The numbers are included only for reference and should not be used to infer relationships between blocks.

When you reference a variable within a JS function, the current scope will be searched.  If the variable is found, great, that’s what will be used.  If it isn’t found, the JavaScript engine will continue to search the scope boxes incrementally outward.  The first match it comes across will be used.

Let’s use the model depicted above…  Let’s say the function for scope #5 referenced a variable called ‘foobar’.  If ‘foobar’ was not found in scope #5, the JS engine would then search scope #3.  If not found in #3, it would search #2, then #1.  Scopes #6 and #4 would not be searched.

What happens if the JS engine reaches the global scope and is unsuccessful in finding ‘foobar’?  The answer to that depends on what you are trying to do with the variable.

LHS vs RHS References

Simpson describes two scenarios for referencing a variable.  He calls them LHS (Left hand side) or RHS (Right hand side) references and they are handled differently in the case of an unsuccessful lookup.

While LHS and RHS refer to which side of the assignment operator (=) the variable is found, this shouldn’t be taken literally, as there are multiple ways to assign a value to a variable.

Rather, think of the difference as such…  LHS is a lookup where we are trying to assign a new value to the variable, whereas RHS is merely looking up the value of the variable to use within your function.

Let’s look at an example:

First, let’s go through this code, find the variables and decide in which scope they reside. There are 2 distinct blocks of scope here.  The global scope and the scope inside the addOne function.

There’s only 1 variable in the global scope and it’s the function addOne. The scope nested inside the addOne function contains 2 variables, sum and a.

Now, where and how are these variables referenced?

There are 2 references to each variable in the addOne function.

Line 1 contains an LHS lookup for a.  It’s LHS because we are assigning the value of 9 to the variable a when the function is called.  Then there is an RHS lookup of a in line 2, as we are only looking up the value of a to use in a+1 (the value of a will not change as a result of this reference).

There’s also one of each type of reference to sum.  The LHS reference to sum is on line 2, where we are assigning the value derived from a+1 to sum.  The RHS reference to sum occurs on line 3, where we lookup and log its value.

The last lookup is on line 6 where we perform an RHS lookup to addOne by invoking the function.

Note: Originally, I mistakenly assumed there was also an LHS reference to addOne on line 1 when we declared the function.  Simpson states in ch. 1, however, that due to the way the compiler handles function declarations, it would be wrong to think of them as LHS references.  Please see YDKJS for further clarification.

RHS… LHS…  Why does it matter?

You may be saying to yourself RHS… LHS… ABC…  XYZ…  Who gives a $*&# as long as I have access to my variable…  The distinction comes when the engine is unsuccessful in finding the variable for which it’s looking.  Knowing the difference now might save you a loooong time in debugging later.

So, what is the difference between an unsuccessful RHS vs LHS lookup? The difference lies in how the engine handles them.

When the engine is unsuccessful in an RHS lookup it throws a ReferenceError.  If the engine does find the variable, but the program is trying to do something with the variable that doesn’t make sense (i.e. invoke as a function a non-function variable), the engine will throw a TypeError.

LHS lookups, on the other hand, are a different story.  If the engine is unsuccessful in an LHS lookup, it simply creates the new variable in the global scope and hands it back to the engine.  As you can imagine, this can lead to all kinds of headaches.

You can prevent this by running in strict mode with "use strict";.  An unsuccessful LHS lookup in strict mode will result in a ReferenceError, similar to that of an unsuccessful RHS lookup.

Let’s look at some examples…

Let’s say we make a typo on line 3 when we are logging sum and mistakenly write sums.

Getting a ReferenceError: sums is not defined tells us that we tried to reference a variable named sums that we failed to declare in a relevant scope.

Another example… Let’s say we try to invoke something that isn’t a function.

Again, the error we get tells us exactly what we should be looking to fix.

Now for an LHS example.   Consider the following:

You can see here that 10 is logged to the console.  Even though we didn’t declare sum as a variable, the engine went ahead and created it for us and the function completed without an error.

What about strict mode?

We can see here that strict mode prohibited the creation of an undeclared LHS reference.

As you can see, knowing the rules of scope and how errors are thrown can significantly cut down your troubleshooting time.

 

I am the scope master!!

Ok, that was a lot to take in, but seemed pretty straightforward.  Is there more to learn on scope or am I now a scope master?

Settle down Yoda…  There are some more advanced topics on scope that I plan to cover in a future post, such as function expressions, IIFEs, hoisting, block scope and the use of let and const.

There are also scope closures to discuss…  I’m not exactly looking forward to writing that one, as closures can be pretty damn confusing.  But, hey, that’s what these rubber ducking exercises are all about, right?

I hope you found this helpful, feel free to leave your comments below.

That’s it for now, I hope this helped straighten out the code…

-Jeremy