Ahhh, scope and closure: two favorite tech interview questions that trip up devs of all ages! Let’s get right to the good stuff (it’s ice cream).
What Are Scope and Closure in JavaScript?
What Is Scope?
Scope is access; that’s the easiest way to think about it. Scope allows you, as a developer, to limit access to certain variables to specific areas.
Scope has two benefits:
- Security : Variables that aren’t accessible from outside the specified scope won’t accidentally get changed later on.
- Naming : Allows you to use the same variable name in different scopes. You know this is helpful if you’ve used let
i = 0
in separatefor
loops in the same function (ahem…).
We often discuss scope in the context of functions, but I found it helpful to first think about scope in the context of a nested object (functions are JS objects anyway…).
Here’s an example of an object with several layers:
Here we have the wildly under-appreciated baked Alaska, which has several layers. If I wanted to know what ice cream is under the meringue, I couldn’t just write bakedAlaska.iceCream
because I would get undefined
. I would have to write bakedAlaska.innerLayer.iceCream
to get to the array of ice creams beneath the meringue. That’s because curly braces make their own scope. So the iceCream array is not defined in the direct scope of bakedAlaska
. It’s defined inside the scope of innerLayer
.
Types of Scope
What Is Global Scope?
This is outside of any functions or curly braces. If a variable is defined in the global scope, it can then be used anywhere in your code (including functions, objects, etc.) Declaring variables in the global scope isn’t considered a best practice because of the reasons listed above.
What Is Local Scope?
When variables are only accessible within a specific context (like a function, or in curly braces), you’ve got a local scope.
What Is Function Scope?
Function scope is a type of local scope. Variables declared inside a function cannot be accessed outside of that function.
function sayMyName(){
let myName = "Julia"
console.log(myName) // "Julia"
}
console.log(myName) // undefined
What Is Block Scope?
Block scope is another type of local scope, and a subset of function scope. Variables declared inside of curly braces are not accessible outside of those braces (just like in the function above).
What Is Closure?
Here’s MDN’s definition: “A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.”
Let’s unpack that.
“In other words, a closure gives you access to an outer function’s scope from an inner function.”
Let’s see this in action using a similar example to the one above:
As you can hopefully see, even though the outer function was invoked on line 14 and has essentially come and gone, the inner greet
function still has access to the variable myName
whenever it’s invoked later. Let’s put this to work on a less contrived example.
Solving “Calculating With Functions” Using Closure
I came across this question only a month or two into my journey as a programmer. My brain melted while trying to come up with a solution and the question sat marinating in the back of my head for months. After re-visiting the subject of closure recently, I realized that was the piece I was missing.
The instructions are to create functions that, when called in the example below, will return the correct answer. For example:
seven(plus(five())); // must return 12
four(times(nine())); // must return 36
Here are some additional stipulations:
- There must be a function for each number from
0
(“zero”) to9
(“nine”). - There must be a function for each of the following mathematical operations: plus, minus, times, dividedBy (
divided_by
in Ruby and Python). - Each calculation consists of exactly one operation and two numbers.
- The most outer function represents the left operand, the most inner function represents the right operand.
- Division should be integer division. For example, this should return
2
, not2.666666
…
My first thought was that I’d have to work from the inside out, since each function would have to wait for its received function to resolve before completing execution. (See my post on call stacks if that doesn’t make sense!)
With that in mind, I started by thinking about the number functions. My thought was that the number function could either be on the outside, taking in an operator function as an argument like this: seven(some operator function here)
, or it could be on the inside, receiving nothing, but returning a number: the five()
of seven(times(five()))
.
So that’s exactly what I did:
Each number function must be able to accept a function as an argument, since an operator function might be called inside, like in the example: seven(plus(five()))
. If you’re familiar with ternary expressions, you could also write the function out like this:
function seven(fn) {return fn ? fn(7) : 7}
I used similar logic for the operations functions. They would need to be able to accept just one number function as an argument plus(five())
. Here’s what stumped me: How were those operations functions getting access to both the outer number and the inner number?
Here’s where the beauty of closures comes in.
I’m going to repeat the logic that’s written in the comments of the snippet above.
- The first thing we do is accept an argument (
x
). In this example, that’s the return value offive()
( i.e.,5
). - Then we define an inner function that utilizes the value of
x
inside of it and accepts an argument of a number. Because the inner function has access to the outer function’s scope at the time of its definition, the inner function will be able to use the value ofx
even after the outer function is long gone. Closure coming in clutch. - Then the outer function returns a reference to the inner function.
Now that we have all the pieces to the seven(plus(five()))
expression, let’s walk through each step, replacing each function with the return values as they get resolved.
- First,
seven(plus(five()))
gets called. - The innermost function,
five()
gets evaluated first. Based on our conditional statement from before, we see that this will just return the number5
. - Now we move out to
plus(5)
. Based on our code snippet, we see thatx
is assigned the value of5
that our inner add function will have access to, and return a reference to the add function. - Then we move out to
seven(add)
. Based on the same function as before, we see that this time a function was indeed passed in. Therefore, the return value is the function add invoked, with the number7
passed in. - Finally, add executes using the remembered value of
5
from its outer scope, and the passed in value of7
, for a return value of12
.
One of my instructors once told me that a good way to picture closure was to imagine that your inner function has access to some variables inside a backpack, which I thought was a really charming and helpful way to think about it.