Getting Started with Scope
There are a few different types of scope that a variable can be a part of, so let’s take a quick, high-level pass at those. It can be helpful to think about the different scopes as a large hierarchy in which some scopes are nested underneath/inside others. We’ll go into this in a bit more detail, but let’s check on the different scopes first.
The first is the global scope. This is the top of the hierarchy, and all the other scopes that get created fall within this scope. If no other scope is created when a variable is declared, then that variable falls into the global scope.
The second scope is function scope; this is the scope that is created when a function is declared. This function creates its own local scope within the global scope in which it exists. Variables that are declared within the local scope of the function are not accessible from outside of that local scope.
A key point here is that while variables declared within the local scope of the function are not accessible to the outer scopes, the variables declared in the outer scopes are accessible inside the local scope!
The third scope is block scope; this is created by a code block, most commonly with
while statements and the like. The variables that are declared within a code block follow nearly identical rules to those for functions with one notable exception.
Variables declared with
const fall within the local scope of the block; they can not be accessed outside of the block. Just as with function scope, the variables declared outside of the block are accessible within it.
The exception here though, is that variables declared with
var are not scoped at the block level, so they are accessible outside of the block!
The rules for hoisting are fairly simple (debatable), so I will try to run through them quickly.
Variable declarations, function declarations, and class declarations are all hoisted (read: effectively, not actually moved) to the top of their respective scope. Function declarations are hoisted above variable declarations, which are in turn hoisted above function calls. Sound a bit confusing? Let’s check it out:
^^…would get hoisted into code that effectively reads like this:
Note how the function declaration is at the top of the scope along with the entire body of the function. Note also that while the
let declaration is hoisted above the function call, the assignment of the variable is not. This behavior leads to a few interesting consequences. The first of which is that when
let declarations are hoisted, the variable that is initialized is not set to a value. This means that if we tried to reference the variable at that point, an exception would be raised.
At this point it is prudent to note that there is a difference between declaring a variable without an initializer and what we see in the hoisted code. Normally, when you only declare a variable with a
undefined to it. This is not what we see with the hoisted code.
Detours aside, there is one other interesting behavior to note about hoisting with variable declarations. When using
var for declarations, the declaration is hoisted just as with
let, but the variable is assigned a value of
undefined instead of being unset!
Other notes, presented in bullet point for brevity’s sake:
- function expressions are hoisted just like variable declarations
- defining methods within non-function blocks has undefined behavior and can cause unpredictable outcomes
- each JS implementation can have a different result for the same code in this situation
There are more facets to hoisting and some interesting edge cases, but we aren’t going to comb through all of them at present.
The last concept we’ll quickly review here is that of closures. Closures are created when a function is defined; they are typically explained as the combination of a function enclosed with references to its lexical environment. Really, what this means is that there are two parts of a closure: the function itself & the collection of references to variables around and within the definition.
It is important to remember that the collection of references that is stored in the function’s briefcase is a collection of references to the variables, not the variables themselves. This is critical for the function to be able to track changes to the variable, whether those be changes to what the variable itself references, or what the variable contains.
In this example,
pet is declared in the global scope and so is the function
checkPet there is a function call that references the variable
checkPet would not have been able to track the reassignment on line 5. However, since the closure packages a pointer to the variable, not the value of the variable, it sees this change and responds accordingly!
Remember that closures create their references from the point of the function’s definition; it doesn’t matter if a variable is in scope when that function is called. As long as a variable is in scope for a function when it is defined, it will be able to access it when it is called.
The rules for closures are generally fairly straightforward, but there are some edge cases and different practical applications; I won’t cover them all here, but I would recommend reading up on Partial Function Applications if you are unfamiliar.
The examples all follow the same basic pattern:
- Example number
- Example code
- Commented answer
- Where applicable, code samples to illustrate hoisting behavior
Remember that the answer is below the example, so scroll carefully if you don’t want to reveal the answers!
If you have any questions, comments, corrections, or recipes, please send them my way!