JavaScript 002: Scope

And how you, too, can keep tabs on your variables!

Terry Hillis
JavaScript in Plain English

--

This is part of a mini series on JavaScript concepts. The expectation is that the readers have minimal experience in JavaScript, so if you’ve been around the block then some of this may be review. That’s not to say that the more experienced folks will walk away empty handed. My goal is to go sufficiently deep into a topic so as to enlighten those whose grasp may be wide but superficial.

TL; DR: variables declared with let and const are block-scoped. Variables declared with var are function-scoped. Never implicitly declare variables, as these will be globally scoped, and will overwrite any other globally scoped values. Lastly, variable names are found by searching the inner-most scope, and if no matching variables are found, the outer scopes will be searched sequentially.

“JavaScript’s global scope is like a public toilet. You can’t avoid going in there, but try to limit your contact with surfaces when you do.”
― Dmitry Baranovskiy

Now that you’ve successfully declared and assigned a community of variables using var, let, and const, it’s time to put them to use. Let’s give it a shot.

let x = 1; function addOne(num){
console.log(num+1);
}
addOne(x); //-> "2"

Marvelous! We successfully accessed the variable x and the function addOne when we called them both at the bottom of the page. But how did we determine that our calls to addOne and x would work? As it turns out, both were in scope when called.

The skinny on scope is as follows: scope is the accessibility of variables in your code.

Each time we call a variable, as we did with addOne(x), the JavaScript engine examines the catalog of variables to which we have access when the call is made. Because the names x and addOne are both accessible in the scope when the function call is made, the code above works with no error. If either names were out-of-scope, we would receive an error.

Let’s modify the code above a bit to see this in action.

{
let b = 2;
const c = 3;
var d = 4;
e = 5;
}
function addOne(x){
console.log(x+1)
}
addOne(b) //-> Reference error, 'b' is not defined
addOne(c) //-> Reference error, 'c' is not defined
addOne(d) //-> "5"
addOne(e) //-> "6"

Whoa! There’s a lot going on here. I’ve added a block using the curly braces {} that encompasses our variable declarations at the top of the code. When we try to access the variables during our function calls to addOne at the bottom of the example, the calls to variables declared with let and const fail, while the others proceed successfully. This is because variables declared with let and const are block-scoped. Their values are only accessible within the enclosing block. If there is no enclosing block, then their values are accessible in the global scope. If our call to addOne occurs within the curly braces, the code works! See:

{
let b=2;
const c=3;
addOne(b) //-> 3
addOne(c) //-> 4
}function addOne(x){
console.log(x+1)
}

Meanwhile, the variable declared with var remains accessible outside of the block. That’s because variables declared with var are function-scoped. Their names are accessible anywhere inside of the function that encompasses them. If they are declared outside of a function, then they are globally-scoped and are accessible anywhere in your code.

Take a look at the following code. Notice how the variable declared with var is accessible anywhere within the function myFunc, but not outside of myFunc.

function myFunc(){
{
var a = 1;
let b = 2;
const c = 3;
e = 5;
} console.log(a); // -> 1
console.log(b); // -> Reference Error, b is not defined
console.log(c); // -> Reference Error, c is not defined
console.log(e); // -> 5
}
myFunc(); console.log(a); // -> Reference Error, a is not defined
console.log(e); // -> 5

This is example outlines another characteristic of JavaScript variables. When declared inside of a function, JavaScript variables are local to that function. That means that their names are only accessible once their parent function has been called, and there names are scoped to their respective block and function.

So, variables declared with let and const are accessible anywhere within their enclosing block, and variables declared with var are accessible anywhere inside their enclosing function. All three variables are globally scoped if there are no enclosing blocks nor functions.

But what about the undeclared variable e ? It, too, can be accessed outside of its enclosing block, and from the above example, e can even be accessed outside of its enclosing function! Variables like e are commonly referred to as implicit globals, and they are particularly insidious to developers.

To demonstrate just why implicit globals are so detrimental to our code, let’s pretend that we have a function called randomConsole that returns a random gaming console. Our function creates a console variable and uses the Math.random() method to randomly select from a list of gaming consoles. The variable is implicitly created and assigned to the result of the random selection and then is returned by randomConsole.

function randomConsole(){
let consoles = ['Xbox', 'PlayStation', 'Wii'];
console = consoles[Math.floor(Math.random()) * 2];
return console;
}
console.log(randomConsole());
//-> Type Error: console.log is not a function

At a cursory glance, one would expect the above code to work in its entirety. Interestingly enough, however, when we try to log the result of randomConsole, we get an error that says that ‘console.log is not a function’. Herein lies the problem with implicit globals. Implicit variables are not local, unlike their explicitly declared counterparts. They exist in the global scope, and will overwrite any other variables that already exist in the global scope — in this case, the console object.

Just to demonstrate that the above function works, lets instead show the value of randomConsole function using the alert() method instead.

function randomConsole(){
let consoles = ['Xbox', 'PlayStation', 'Wii'];
console = consoles[Math.floor(Math.random()) * 2];
return console;
}
console.log('test') // -> 'test'
alert(randomConsole()); //-> 'PlayStation' is alerted!
alert(console); // -> 'PlayStation' is alerted, too!

Notice how we can still use the console.log method, even after the randomConsole function is declared? This is because variables that are defined inside of a function are only declared once the function is called. When we try to access the console object in the last alert in the above example, we return ‘PlayStation’, because the randomConsole function was called, and because the console object was reassigned during that call. This behavior is not something that you have to worry about if you explicitly declare all of your variables.

The power of scope lies in the fact that we can control the accessibility of variable names in our code. Because of locally scoped variables, we don’t have to worry about one functions’ variables stepping on the toes of another functions’ variables. This significantly lessens the burden of tracking variable names that are declared throughout our code. We can cheerfully reuse variable names across our functions without any hesitation that our variable references will be misunderstood by the JavaScript engine.

There are a few exceptions to this rule, such as what happens when there are multiple levels of functional and block scoping. In these circumstances, when a variable is accessed in an inner layer block, any outer layer blocks that hold the same variable name will be disregarded. Only the inner-most variable declaration and assignment is accessed. You can see this in action in the following example.

let fruit = ['raspberry', 'tomato']; 
let vegetables = ['celery', 'spinach'];
}
let fruit = ['apple', 'blackberry', 'orange'];
console.log(fruit); // -> ['apple', 'blackberry', 'orange']
console.log(vegetables); // -> ['celery', 'spinach'];
}

The global fruit and vegetable variables are accessible inside of the block. However, the fruit variable that’s declared inside of the block takes precedence over the global fruit variable when the variable lookup occurs during the console.log . This happens because of the protocol that JavaScript takes names when variables are looked up. JavaScript always searches the immediate scope for variable names first. If no matching names are found in the current scope, JavaScript then searches the next level of enclosing scope. In the above example, when fruit variable is looked up, the block-scoped fruit variable is found in the immediate scope, and the look up process terminates. When the vegetable variable is looked up in the immediate scope, no matching variable name is found. JavaScript then searches in the next encompassing scope, where it finds the globally available vegetable variable. This is a subtle, but powerful, feature of JavaScript scoping that allows us to control the namespace of our variables in embedded functions and blocks. If you ever wonder which variable assignment takes precedence, just remember that inner definitions take precedence.

To summarize: variables declared with let and const are block-scoped. Variables declared with var are function-scoped. Never implicitly declare variables, as these will be globally scoped, and will overwrite any other globally scoped values. Lastly, variable names are found by searching the inner-most scope, and if no matching variables are found, the outer scopes will be searched sequentially.

Thanks for reading, folks. Next up: closure

--

--