Closures sounds like some fancy complex concept, but the idea is simple. It all boils down to scope and how javascript deals with it.
Just to make things clear. The example itself could of been written a slightly more “consistent” way. There are a number of ways of writing this code that performs the same (at least in this context), and really the original programmer choose a “mixed” approach IMO. The following are alternatives of writing the same code basically.
Either:
// the "classic" oldschool way
function multiplier(factor) {
return function (number) {
return number * factor;
}
}
let twice = multiplier(2);
console.log(twice(5));
// → 10
And the “syntax-surgar” way, using only arrow functions
const multiplier = factor => {
return number => number * factor;
}
let twice = multiplier(2);
console.log(twice(5)); // 10
or the super syntax sugar concise way
const multiplier = factor =>
number => number * factor;
const twice = multiplier(2);
console.log(twice(5));//10
Now ontop the idea of closures, which can be abused to do stuff like this:
console.log(whoa(1)(2)(3)); // [1,2,3]
You might be like “whoa is that even code?” and yea, JS is funky like that.
The definition is closures are functions that have access to an outer functions scope, which might be confusing, but as @RadDog25 said, it’s much simplier once you figure out the concept. But first lets look at the implementation of the above code which will be used in the breakdown in a bit:
// remember, arrow functions are syntax sugar over normal functions
const whoa = (first) => {
return (second) => {
return (third) => {
return [first, second, third]
}
}
}
console.log(whoa(1)(2)(3)); // [1,2,3]
So, the easiest way to get around to understanding closures is to imaging yourself as the js runtime going over this code. What makes it kinda confusing is your basically “reading up” the code when your executing it, instead of the usual top down. So let’s follow the JS runtime step by step as it goes over this code.
- It reads the script top to bottom.
- This step is somewhat obvious, first it sees the
const whoa
and sees its defined as a function, and dives into it to see how its defined. That’s all fine and good as it comes out of all the nested functions “knowing” how the functions are defined. Theres a reason why I wont go “into it” because technically the code within these functions is just a “value”, but that value just happens to be a function. Functions don’t “do” anything until they are called. Now you might be like “But aren’t they called yet” and Ill say no, they have only been defined. We call them later.
- The final line (
console.log(whoa(1)(2)(3))
) is were we actually start “execution”. It all starts with a rather innocent character (
after .log
You might not think much about this, but just like math, parenthesis basically are telling the js runtime to “execute and evaluate whats in-side”, and whats inside is this code: whoa(1)(2)(3)
. After executing this value, we pass the value of whoa(1)(2)(3)
to .log
to print out. Nothing fancy here, whoa
could of been a string and we’d be done, but were not.
- Now we execute the `whoa(1)(2)(3) code, and this is where thing start going into what we defined at the start.
- With this function we find another
(
(just left of the 1) which again tells the JS runtime to “execute” the function essentially. Now looking at the whoa
function definition we see basically this:
whoa = (first) => {
return <VALUE>;
}
Where the <VALUE>
is the inner function. That’s fine, what we know about functions is we return whats after the return
on the same line, so lets do that and see how the whoa
code in the .log()
statement looks to see if were “done”:
console.log(<VALUE>(2)(3))
Now you might notice something here, if <VALUE>
was the name of a function it would look like: myFunction(2)(3)
and we’ve already seen that before… But lets go onto the next "step
4. We need to execute the next (
after our <VALUE>
code, and lets get real <VALUE>
is a function right? So we execute the “inner function”.
Now our inner function (the one with second
) is boring too, as it’s defined just as:
(second) => {
return <VALUE_2>;
}
And again <VALUE_2>
is actually just another function. and the “value” within the .log
looks more or less like this:
console.log(<VALUE_2>(3)
where the <VALUE_2> is the result of the the
whoa` function AND it’s inner function!
- We proceed execute the following for the final nested function:
(third) => {
return [first, second, third]
}
Now this is where the definition of a closure comes in, and where everything comes together.
Within this code snippet the JS runtime has no idea what first
or second
is, it only knows what third is, as it’s passed. (in our whoa example it just happens to be 3) So it looks outside of the current scope of code to see if it can find one first. So it goes to the second code snippet:
(first) => {
// first is here!, and it was executed way back earlier with the value of 1
(second) => {
// no "first" in this scope
return (third) => { // third is passed as 3
return [first, second, third];
};
}
}
So it adds the “first” value (1) into the returned array after looking into the first functions scope, even tho the second inner function didn’t have it
Now this is where closures will make more sense, if the first
variable didn’t exist in my code at all the JS runtime will go to the global scope (outside of all the code I’ve written) and see if it exists there The global scope can have the variable as well, so you can essentially think of all your code running in a “global” function (!!!)
- Now to finalize this lets get the value for the second part of the array, which only “looks up” to the parent inner function of where we are currently “executing” (just as a reminder were’ still “in” the third inner function.
(second) => { // second had a value of 2 remember?
// Oh we only had to look up to the scope "above" or "outside" of ours
return (third) => {
return [1, second, third];
};
}
- We add our “third” value into the array and return it:
(third) => { // we passed 3 as our last (3)
return [1, 2, 3];
};
- Now finally we return this value back to console.log and exit our script.
console.log([1,2,3])
So to summarize this long post, closures can access the outer scopes to “get stuff” from the outer scopes, in the same way you can access stuff in the global scope. It also matters that your “inside” of something, as my whoa
example shows when you execute functions that return functions and access the parent scopes at the same time.
Hopefully that helps some by providing a rather verbose explanation of a rather unusual code example.
PS here’s the codepen for the above code if you wanted to play around with it, it also has some more console.logs