Introduction
Scope and context may seem similar, but they actually have very different functions.
Simple principles govern how code is structured and interpreted by computers. These principles build off of each other, repeatedly compounding to create more complex and efficient applications. This article will describe two of these principles in particular: scope and context. Although these two principles are undoubtedly important in other coding languages, I will focus on their most basic behaviors in JavaScript specifically.
Scope and context can seem very similar, and if you get them confused from time to time, don’t feel alone. Like many aspects of coding, scope and context can be difficult to visualize and can hinder JavaScript beginners from advancing their capabilities as quickly as they’re capable of.
One possible reason for this confusion is because scope and context both refer to when variables or certain keywords become relevant, and what exactly they refer to when they’re called upon. Let’s take a closer look at scope first.
Scope
Before diving into what the scope is, let's try an experiment that demonstrates how the scope manifests itself.
Let's say you declare a variable greetings:
const greetings = 'Hello';
console.log(greetings); // Hello
Then, this variable can be easily console
in the next line after the declaration giving the expected message Hello
at the console. No questions here.
Now, let's move the declaration of greetings inside an if statement code block:
if (true) {
const greetings = 'Hello';
}
console.log(message); // ReferenceError: greetings is not defined
This time, when trying to log the variable, JavaScript throws ReferenceError: greetings is not defined.
Why does this happen?
The if statement code block creates a scope for greetings variable. And greetings variable can be accessed only within this scope.
At a higher level, the accessibility and availabilities of variables is limited by the scope where they're created. A variable declare within its scoped can be access But outside of its scope, the variable is inaccessible.
Now, let's put down a general definition of scope:
The scope is a policy that manages the accessibility of variables.
In the most simplified of terms, there are two main types of scope: global scope and local scope.
Global scope refers to variables that are accessible anywhere because they are declared outside of any individual functions or methods (usually at the top of the file). They exist in the global space, which make them ready to be called upon at any time.
Local scope refers to variables that are accessible within the boundaries of their function.
let animal = "Lion";
console.log(animal, "is the king of animals"); // Lion is the king of animals
function showScopeExample() {
let animal = "Tiger";
}
showScopeExample();
console.log(animal, "is also a wild animal"); // Lion is also a wild animal
In this example, we have our initial declaration of the variable animal at the top of our file, which is assigned to the string of “Lion.” Immediately below, we console.log
the animal
variable, as expected, this first console.log
prints ‘Lion,’ as seen on the right side of the code. Then, we have a function of showScopeExample(), which reassigns our dog variable to the string of “Tiger.”
This function is immediately invoked, but when we console.log
“animal”
for the second time, it still console as “Lion.” Even though the second console.log
occurs after we reassign the variable inside of the function, the animal variable is not currently capable of returning as “Tiger”.
Our re-declaration of animal inside of showScopeExample()
is locally scoped. The easiest way to know this is to look at its location. The re-declaration lies in between opening and closing curly brackets, which act as barriers to the relevancy of these variables.
Limiting the accessibility of the variables declared inside of functions allows us to reuse variable names that may apply to separate parts of our code. Locally scoped variables also prevent our code from having tens or hundreds of global variables floating around the global space unnecessarily. It’s also good practice to exercise proper abstraction in our JavaScript by limiting access to variables to areas where they are needed.
let animal = "Lion";
console.log(animal, "is the king of animals"); // Lion is the king of animals //1st console
function showScopeExample() {
let animal = "Tiger";
console.log(animal, "is also a wild animal"); // Tiger is also a wild animal //2nd console
}
showScopeExample();
console.log(animal, "is the king of all"); // Lion is the king of all // 3rd console
In this example, all we’ve changed is the location of the second console.log, which is now inside the boundaries of the showScopeExample() function. As seen in the console to the right, the first console.log
still prints “Lion” as expected.
Unfortunately, our second console.log
now prints “Tiger” when we invoke showScopeExample() (poor pup). This is because our second console.log is now held within the boundaries of the curly brackets, and it will print information that is located within the same scope.
Finally, the third console.log returns to printing “Lion.” This is again because it does not have access to the local re-assignment and resorts to the initial declaration.
Other types of scope:
Module scope
ES2015/ES6 module also creates a scope for variables, functions, classes.
The module circle defines a constant pi (for some internal usage):
// "circle" module scope
const pi = 3.14159;
console.log(pi); // 3.14159
// Usage of pi
pi
variable is declared within the scope of circle module. Also, the variable pi
is not exported from the module.
Then the circle module is imported:
import './circle';
console.log(pi); // throws ReferenceError
The variable pi
is not accessible outside of circle module (unless explicitly exported using export).
The module scope makes the module encapsulated. Every private variable (that's not exported) remains an internal detail of the module, and the module scope protects these variables from being accessed outside.
Looking from another angle, the scope is an encapsulation mechanism for code blocks, functions, and modules.
Lexical scope
Let's define 2 functions, having the function innerFunction() is nested inside outerFunction().
function outerFunction() {
// the outer scope
let outerVar = 'I am from outside!';
function innerFunction() {
// the inner scope
console.log(outerVar); // 'I am from outside!'
}
return innerFunction;
}
const inner = outerFunction();
inner();
Look at the last line of the snippet inner(): the innerFunction() invocation happens outside of outerFunction() scope. Still, how does JavaScript understand that outerVar inside innerFunction() corresponds to the variable outerVar of outerFunction()?
The answer is due to lexical scoping.
JavaScript implements a scoping mechanism named lexical scoping (or static scoping). Lexical scoping means that the accessibility of variables is determined statically by the position of the variables within the nested function scopes: the inner function scope can access variables from the outer function scope.
A formal definition of lexical scope:
The lexical scope consists of outer scopes determined statically.
In the example, the lexical scope of innerFunction() consists of the scope of outerFunction().
Moreover, the innerFunction() is a closure because it captures the variable outerVar from the lexical scope.
Variables isolation
An immediate property of scope arises: the scope isolates the variables. And what's good different scopes can have variables with the same name.
You can re-use common variables names (count, index, current, value, etc) in different scopes without collisions.
foo() and bar() function scopes have their own, but same named variables count:
function foo() {
// "foo" function scope
let count = 0;
console.log(count); // 0
}
function bar() {
// "bar" function scope
let count = 1;
console.log(count); // 1
}
foo();
bar();
The scope is a policy that manages the availability of variables. A variable defined inside a scope is accessible only within that scope, but inaccessible outside.
In JavaScript, scopes are created by code blocks, functions, modules.
While const and let variables are scoped by code blocks, functions or modules, var variables are scoped only by functions or modules.
Scopes can be nested. Inside an inner scope you can access the variables of an outer scope.
The lexical scope consists of the outer function scopes determined statically. Any function, no matter the place where being executed, can access the variables of its lexical scope
Context
Context in JavaScript is another subject of confusion. It refers basically to the use of the keyword this
. The value of this
depends on where it is being invoked.
Invoking this
at the global space will return the entire window object. This happens because the window object is the starting point of all the code we write. Give it a try, and you’ll likely get something like this in response…
console.log(this);
The endless text we get in return is the window object. The first rule of this
is that:
by default, it refers to the window object.
If you ever get the chance, you should explore the window object returned from invoking this in the global context.
But what happens if we invoke this somewhere other than the global context? If we invoke this in the context of a new object, it will return that object, just as it returned the entire window object. Let’s look at a simple example and then build on it.
//input
const newContextObj = {
invokeThisInNewContext () {
return this;
},
secondFunctionContext() {
return " I exist in this context too"
}
}
newContextObj.invokeThisInNewContext()
Output
In the example above, we’ve created a new object which we’ve named newContextObj
. When we invoke the method invokeThisInNewContext()
it returns the entire newContextObj
.
This is the same as when it returned the window object in our first example. The only difference is that the window object is a very large and complex object, and our newContextObj
is very small and simple. The second rule of this is that:
when
this
is invoked in the context of an object, this will refer to that object.
Finally, the third rule of this is a bit more complicated. In JavaScript, we use classes to create multiple instances of objects that share properties, even though those properties may differ in value.
For example, we could have a class that produces instances of a car object. Despite the fact that the cars all have sizes, capacity, and engine types, these three things probably differ from car to car. Below is an example of this being invoked in the context of an instance of ContextObj
.
//input
class contextObj {
constructor(here, now) {
this.context = here;
this.time = now;
}
whereIsThis() {
return this;
}
whatTimeIsIt() {
return "Time to get a watch."
}
}
let contextObj = now contextObj ( "rightHere", "rightNow");
contextObj.whereIsThis();
Output
What we see in the example above is the invocation of the whereIsThis()
method on our instance of ContextObj
. In this third and final example, the keyword this
refers to this specific instance of ContextObj
, no matter how many instance of ContextObj
exist in our code.
Conclusion
These three examples attempt to summarize this in its most basic forms, and you’ll find in your own experiences that this
becomes a very handy tool to have at your disposal. As a javascript developer, you can surely appreciate what an important topic this is, and how critical it is to understand the difference between scope and context in JavaScript. These two subjects become very important as soon as you have to write or edit even intermediate-level JavaScript, and your ability to comfortably and confidently write / edit JavaScript will only improve once you have a good working knowledge of the difference between scope and context. The good news is, it’s not that difficult to understand and once you get it, you’ve got it. The best way to become more familiar with concepts such as context and scope is simply experimenting with them.
Open your preferred text editor and start creating functions and object of your own. Play around with variable placement in your functions. When do you have access to them and when do they return undefined
?
Create increasingly complex objects and invoke this in increasingly specific and limited contexts. There is no better way to become comfortable coding than by getting your fingers on the keyboard and struggling through concepts, one at a time.