One of the biggest buzzwords in the JavaScript language is closure. It’s the subject of many job interview questions at FAANG companies. In this article, we’ll talk about closure and scope, illustrate its concepts with simple examples, and then finish out with a sample question from an interview with one of the bigger tech giants.
Scope
When someone tells you something is or isn’t in scope of a project, what does that mean?
I’d like to think of a periscope or a telescope when I think of the answer to this. These instruments show us all sorts of things within the confines of the lens it has: it’s in scope. If it’s outside the scope, you can’t see past the diameter of the lens. And shining a light on something outside the diameter is not possible. You should be thinking about this as we talk about three very important and distinct types of scope in JavaScript: local, global and lexical.
Local Scope
Local Scope is the smallest of the three scopes we will be talking about today. When we declare a function, anything inside the brackets ({}) is considered to be local to the function. When the JavaScript engine reads the function it will declare the variables; when it ends it will destroy the variables.
function greeting() { var websiteName = 'Career Karma'; return `Hello ${websiteName}`; } console.log(greeting()); // Hello Career Karma console.log(websiteName); // ReferenceError: websiteName is not defined
As you can see, when we “console.log()” the outcome of the invoked greeting function, we are able to access the websiteName after the function was executed. This gives us the ‘Hello Career Karma’ string that we were looking for. The console.log()
of the variable that was declared inside the function throws an error because it’s not defined.
As mentioned already, the reason why websiteName is undefined is because variables are created inside functions when they are invoked and then destroyed when the terminal statement runs. Anything outside of the function does not have access to stuff inside the function unless it has a special setup.
Global Scope
This next scope is pretty much a literal translation of the phrase. A global scope takes the items declared outside of a function and reserves them in a space where all the scripts and methods and functions can access and use them for their logic.
let counter = 0; // global -- declared outside function const add = () => { // function declaration let counter = 0; // local -- declared inside function counter += 1; // counter increased by 1 -- which counter variable increased? return counter; } add(); // invoke add(); // three add(); // times console.log(counter) // is this 3 or 0? Why?
What does the code above do if we console.log()
the counter at the end of the code? What do you expect to happen?
Let’s walk through the code:
- Counter variable declared and initiated in the global environment.
- Add function declared in the global environment.
- Add is invoked.
- Counter variable declared and initiated in the local environment.
- The local counter increases by 1 ⇐ why local and not global?
- Counter is returned. Function terminates.
- Add is invoked again
- Walk through steps 4 to 6 again.
- Repeat steps 3 to 6 again.
console.log(counter)
; ⇐ What is returned?
Because the function terminates when the counter is at 1 every time, our local counter variable is redeclared and re-initiated at 0 every time the function runs. No matter what happens, the counter will always stop at 1 on the local level.
If a function finds a variable within its scope, it doesn’t look to the global scope for the variable – so the global variable never changes. So, our console.log()
will output 0 as our closest defined variable within that statement’s environment is in the global environment.
Lexical Scope
The lexical scope is one of the most fundamental concepts in JavaScript. It’s the idea that the creation of a function or of a variable will be accessible to certain parts of the code and then inaccessible to other parts of the code. It all depends on where the declaration of each variable and function is.
Let’s take a look at this block of code:
const init = () => { // <== This is our outer function const var1 = 'Career'; // outer scope const second = () => { // <== This is our inner function const var2 = 'Karma'; // inner scope console.log(var1); // Career console.log(var2); // Karma return var1 + " " + var2; }; // console.log(var2); // undefined return second(); }; init();
Here we have a set of nested functions. The init()
function declares a variable called var1, declares a function called second and invokes second()
.
When the compiler passes through this code the first time, it takes a high level look at what we have:
init()
function- invoke
init()
At this point, we can’t see anything else inside the init() function – we just know the function exists. When our init() func is invoked, the compiler takes another high level look at what’s inside the function:
var1
second()
function- invoke
second()
The init()
function knows nothing about what is going on inside the second()
block. It can only see what is in its lexical environment – its surrounding state.
Each nested function is in a smaller container, like a set of those Russian matryoshka nesting dolls (see top of page for example if you’re unsure of what they are). The dolls only know about what’s going on inside their container and what’s already occurred or declared/read in the parent. The largest doll for example only knows that the next doll in its container exists. It doesn’t know about any of the other dolls in the set, just what’s in its lexical environment (its state) and what’s already happened (the outer scope).
In essence, we know two things:
- The outer scope can’t see the inner scope.
- The inner scope has access to the outer scope.
Because the outer cope can’t see what’s going on in the inner scope, we can safely say that this is a one-way relationship. Inner can see and use variables from the outer scope, but outer can’t see inner. This is called lexical scope.
The beauty of lexical scoping is that the value of a variable is determined by its placement in the code. The functions look for the meaning of a variable inside it’s local environment first – if it can’t find it, it moves to the function that defined that function. If it can’t find it there, it moves up the chain to the next defined function.
This becomes a very important concept in JavaScript that will come up time and again as you learn more about JavaScript frameworks and how they work. You can pass down from the outer, but you can never pass “up” in the other direction. This is very important as we get to the major topic at hand: closure.
Closure
Closure’s definition is very similar to that of lexical scope. The main difference between the two is that closure is a higher order function and lexical scoping is not. A higher order function has one basic characteristic: it either returns a function or uses a function as a parameter.
Closure is a function that can access its lexical scope, even when that function is being invoked later.
Both closure and lexical scope have their own variables, can access a parent function’s variables and parameters, and can use global variables. Let’s walk through the following code:
function greeting() { //outer scope (parent function) const userName = "CrrKrma1952"; // parent variable function welcomeGreeting() { // inner function console.log("Hello, " + userName); // accesses parent var return "Hello, " + userName; // terminal statement } return welcomeGreeting; // returns a function (which makes it HOF) } // end of greeting() const greetUser = greeting(); // greetUser(); // Hello, CrrKrma1952
greeting()
function exists, but we don’t know the contents yet.greetUser
exists, but don’t know contents yetgreetUser()
– this invokes the previous line, which, in turn, invokes the greeting() function.- userName declared
welcomeGreeting()
exists, but don’t know contents yet- Return statement below the
welcomeGreeting()
block returns that very same function console.log(‘Hello, ‘ + userName)
; Our console.log here can access the parent scope to get the value of userName- Terminal statement that ends the function and destroys the meaning of the variables inside the code block.
In this code, we are passing around information by nesting functions together so that the parent scope can be accessed later.
Conclusion
In this article, we talked about a pretty hefty JavaScript subject: Scope and Closures. I would recommend branching out and reading several different articles on the subject. The way this is taught can come from a variety of points-of-view – which means, of course, that there’s lots of ways to learn it. I hope this primer was helpful to you! Good luck in continuing your study on closures!
"Career Karma entered my life when I needed it most and quickly helped me match with a bootcamp. Two months after graduating, I found my dream job that aligned with my values and goals in life!"
Venus, Software Engineer at Rockbot
About us: Career Karma is a platform designed to help job seekers find, research, and connect with job training programs to advance their careers. Learn about the CK publication.