Introduction
To become confident as a JavaScript programmer or developer, you always need to know how everything in your code works. With that understanding, you can avoid certain errors in JavaScript. An error that tends to occur due to limited knowledge of how scoping work is the "referenceError".
For context, "referenceErrors" are errors that can occur when we work with a variable outside its scope.
Scoping refers to how the variables of our program are organized and can be accessed.
JavaScript works in such a way that variables aren't just accessible anyhow and anywhere. They are defined in scopes which dictate how accessible they are throughout the code .
In this article, I will go over all the types of scope and the errors commonly encountered accessing a variable outside of its scope.
Understanding scoping is particularly important because this helps us write clean, maintainable, and well-structured codes.
prerequisite
A basic knowledge of JavaScript will be enough to follow through on this article.
What is a Scope?
A Scope is a space or environment in which a certain variable is declared.
Where we declare our variables and functions will invariably influence where they can be accessed. The scope of a variable is thus, the region of our code where we can access the variable.
Types of Scope
In JavaScript, Scope can be broadly classified into;
Global scope
Variables declared outside of a function or block(curly braces) are said to exist in a global scope and are accessible anywhere in the code.
const lastName = 'Willams';
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
console.log(lastName);
return age;
}
console.log(calculateAge(2000));
console.log(age) // Uncaught ReferenceError: age is not defined.
In the above code snippet, lastName can be regarded as a global variable because it was declared outside of the function and block/curly braces, and can be accessed anywhere. Printing it to the console from the calculateAge
function works fine without any error because it is a global variable you can access from anywhere. This is not the same for variables declared in a function such as the age variable above. If we log the age variable to the console from outside the function, we get an error: Uncaught ReferenceError: age is not defined.
You might be thinking, why is that? Since It is defined there in the function, why are we getting undefined? We will get there soon.
The lastName variable is a global variable and can be accessed anywhere. However, if it was initialized below where the function is being called or anywhere it is being used we get an error; Uncaught ReferenceError: Cannot access 'lastName' before initialization
, this is because it is being used outside of its scope. You can check this out by moving the position of the 'lastName' variable below the function call. It is important at this point to reiterate that the scope of a variable is the region of the code where a certain variable can be accessed. Although a variable exists in the global scope, its scope starts where it is initialized.
Local Scope
Initially, local scope refers to variables declared inside a function only. However, with the introduction and adoption of Es6(the new version of writing JavaScript), local scope refers to both function scope and block scope.
Function scope
Each function creates a scope during execution and the variable declared in that function is only accessible within the scope. Outside the function, the variables are not accessible.
In the code snippet we saw earlier under the global scope, the age variable was logged to the console when we called the function calculateAge,
because it was declared in the function and called from it.
if we perform an operation with the age variable outside the function(its scope), such as logging it to the console in the global environment as we mentioned earlier, we get an 'Uncaught ReferenceError: age is not defined'
because that is outside its scope.
Although we have seen that variables declared within a function scope can only be accessed within the function, all scope has access to variables from the outer scope or parent scope. This is the concept of lexical scoping in JavaScript.
Does it sound abstract? Let’s visualize it with some code snippets below.
const lastName = 'Willams';
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
function printAge() {
const output = `${lastName}, you are ${age}, born in ${yearOfBirth}`;
console.log(output);
}
printAge();
return age;
}
calculateAge(2000);
console.log(age) // Uncaught ReferenceError: age is not defined.
We created a second function printAge()
inside the calculateAge()
function. In it, we created a variable, output which is a string literal with the value of the variables, 'lastName', 'age' and 'yearofBirth'. Then we logged the value of the output to the console and called the printAge()
function.
Think for a minute, why aren’t we getting any errors, since we called the 'age' and 'yearofBirth' variable which is not a global variable and is not being defined in the printAge()
function?
That is because javascript performs a variable lookup, which is a process where one scope looks at the scope above/parent scope for a variable called in it.
Although the 'age' variable is not a global variable, it is defined in a function which is a parent to an inner function, so it can be accessed from that child/inner function.
The printAge()
function is a child of the calculateAge()
function. Thus, it has access to variables declared in it.
This variable look-up can only happen one way up, that is, a certain scope will never have access to its child scope or the scope below it. The output variable is declared in printAge()
, a child function of calculateAge()
, If we log to the console, the output variable from the calculateAge()
function, we get an error Uncaught ReferenceError: output is not defined at calculateAge().
This happens even though it is the parent, further buttressing the fact that variable look-up can only happen one way up and the parent can not access variables from the children.
Block scope
Starting with Es6, variables declared inside a block( curly braces {}) which include for loop and the if block is only accessible within the block. Block scope is very similar to function scope and behaves the same way. Everything explained for function scope is true for block scope, the only difference is that this applies to variables declared with let and const keywords only. Variables declared with let and const are block-scoped. However, variables declared with the var keyword are still accessible outside of a block and are function-scoped.
We can demonstrate that with a code snippet below:
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
function printAge() {
const output = `${lastName}, you are ${age}, born in ${yearOfBirth}`;
console.log(output);
if (yearOfBirth <= 2005) {
const str = `Oh you are an adult ${lastName}`;
console.log(str);
}
}
printAge();
return age;
}
calculateAge(2000);
In the above code snippet, we created a string inside the conditional if block and logged the result to the console right from the if block. If we move the console.log(str)
away from the if block but still keep it within the printAge()
we get an error: Uncaught ReferenceError: str is not defined at printAge () and at calculateAge()
, because the scope of the variable “str” is just within the if block. The var variable however is not block-scoped and though declared inside a block, can still be accessible outside it.
const lastName = 'Willams';
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
function printAge() {
const output = `${lastName}, you are ${age}, born in ${yearOfBirth}`;
if (yearOfBirth <= 2005) {
var adult = true;
const str = `Oh you are an adult ${lastName}`;
}
console.log(adult);
console.log(str) // throws an error
}
printAge();
return age;
}
calculateAge(2000);
In the snippet above, we declared the var variable adult inside the if block and when we accessed it from outside the block but within the printAge()
function, we don't get an error. This is because the var variable is function-scoped. Though declare in the if block, it can still be accessed from the enclosing function.
The scope chain
Initially, I said when a variable that is called in a scope is not declared there, JavaScript performs a variable lookup to the parent or enclosing scopes and global scope to look for the value of the variable. However, if they exist variables with the same names but different values different scopes, priority is always given to the variable closer to the scope it is being called. In other words, JavaScript looks up the scope chain for the value in the closest scope.
const lastName = 'Willams';
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
const lastName = 'Stephen';
function printAge() {
const output = `${lastName}, you are ${age}, born in ${yearOfBirth}`;
console.log(output);
}
printAge();
return age;
}
calculateAge(2000);
Here, we can see there are two lastName variables, albeit defined in two different scopes. In the printAge()
function the value of the lastName variable is being called, so which value will it take on? Let’s think about this for a minute. What will be the value and why?
JavaScript while executing the function checks for the value of the variable within the scope. When it doesn’t find it, it moves upward to the immediate parent scope and checks for the variable. If it finds it there, JavaScript takes the value and stores it where it is being called in the printAge()
function.
If it isn’t found, it looks one step ahead which in this case is the global scope and uses the value given there.
Notice how declaring a variable using const with an existing name didn’t return an error? As we said, this is because they exist in different scopes, the const variable is blocked-scoped and as such are different entities with the same name. If we call the variable LastName outside the functions we’ll get the value assigned in the global variable which is Williams.
However, we are capable of reassigning a variable declared in a parent scope inside a child scope, using the let keyword of course because we can’t reassign variables with the const keyword.
In the code snippet below we will reassign the last name variable in the calculateAge()
function and log it to the console from the global scope to see if we will get the initialized value in the global scope or the reassigned value from the function.
let lastName = 'Willams';
function calculateAge(yearOfBirth) {
const age = 2023 - yearOfBirth;
lastName = 'Stephen';
function printAge() {
const output = `${lastName}, you are ${age}, born in ${yearOfBirth}`;
console.log(output);
}
printAge();
return age;
}
calculateAge(2000);
console.log(lastName);
If you follow along in your editor you will see that the lastName variable logged to the console is the reassigned value and not the value initially given in the global variable.
With this, we can say a child scope has the power to reassign a variable declared in the global variable.
Conclusion
Well done for going over the concept of scoping in JavaScript. Though this article focused on variables, functions can also be scoped. For example, the printAge()
function was declared in the calculateAge()
function and called from it, if we move the calling of the function outside the outer/parent function, we will get a “not defined “error, because it is being called outside its scope. Also, I said variables cannot be accessed before initialization, functions are slightly different in the sense that with function declaration a function can be called before it is defined. variables with the var keyword can also be accessed before initialization but the value is always undefined. This is the concept of Hoisting in JavaScript.
To get a comprehensive or broader knowledge of scoping it is pertinent to understand closure and how it works. Unfortunately, that is beyond the scope of this article though(pun intended). I have however curated more resources to help you learn.