Understanding closures once and for all
- JavaScript
As you keep acquiring knowledge about JavaScript, some fundamental concepts arise. You have probably been unconsciously implementing some of them, like closures.
Closures can be a bit tricky to understand, specially since if you google what it is, some of the explanations about it can be hard to reason about. But don't worry, in this article I'll do my best to explain what a closure is in a simple way, providing some easy to understand examples.
Before we dive into what a closure is, let's define some important concepts we should also know about:
The scope
The scope basically refers to the locations/contexts from which a variable or function can be accessed.
Consider the following example:
// global scope let x = 5 const someFunction = () => { console.log(x += 5) } someFunction() // 10
The x variable is declared in the global scope, therefore it's considered a public variable that can be accessed within any function or by any other variable. Let's see another example:
// global scope let x = 5 const someFunction = () => { // someFunction's local scope let y = 3 console.log(x += 5) } someFunction() // 10 console.log(y) // ReferenceError: y is not defined
Here, we are declaring a y variable inside someFunction's body, this variable is in a local scope that belongs to this function, so it can't be accessed outside of it. It's also known as a private/local variable.
Lexical scope
The lexical scope (also known as static scope) represents the location in which a variable or function has been defined in the source code. The term "lexical" implies that the access to the variables or functions is determined by the lexical structure or context of the code rather than dynamic factors such as how and when functions/variables are called/accessed at runtime. Let's see an example:
// global scope const address = 'Front-end street, JavaScript avenue' const getAddress = () => { // getAddress' local scope return address }
Here we can see that address is being declared in the global scope and gets accessed by getAddress function. In this case, the lexical scope of address is the global scope because it's the place in which it was created in the first place.
Now that we know about the scope and lexical scope, and if we analyze all the provided examples, we can tell that the place in which a variable or function is declared will determine the code that is permitted to access it. In other words, only code within a variable or function's lexical scope will be able to access it.
Now, you might be thinking: why is this guy explaining all this stuff? I'm here to learn what a closure is!
Don't worry, we are almost there. The reason for explaining all of this, is because it's important to have these concepts very clear in order to fully understand closures.
What's a closure?
A closure is a child function declared inside a parent function that has access to its parent's scope even after the parent function has closed. Note that a closure is created when the function is defined, not when it's called/executed.
Let's see it with an example:
// global scope let x = 5 const parentFunction = () => { // parentFunction's local scope let y = 3 const childFunction = () => { console.log({ x: (x += 5), y: (y += 1) }) } return childFunction } const result = parentFunction() result() // { x: 10, y: 4 } result() // { x: 15, y: 5 }
Notice that we are not calling childFunction inside parentFunction, we are just returning it. In fact, if you just call the parentFunction, you won't see the values being logged in console inside the childFunction.
Also notice that parentFunction already has closed/returned, yet when we call result later on, the childFunction is executing and adding 5 and 1 to x and y respectively each time. We can say that the childFunction has closure over both parentFunction's scope and global scope. Think of it as taking a picture: when the childFunction was declared, it took a picture of what the parentFunction's scope looked like and also, in this picture there is a nice, beautiful background that was also portrayed, it's the global scope.
Let's see another example:
Imagine that there is a boy and a girl in a candy store, their parents give each one $3.00 to buy 3 candies (each candy is worth $1.00) they go ahead and buy their candies as follows:
const displayDollarsLeft = (dollars, child) => { const noun = dollars > 1 ? 'dollars' : 'dollar' const dollarsLeftMsg = `${dollars} ${noun} left` const discountDollars = `Discounted 1 dollar to the ${child}... ${dollarsLeftMsg}` const noMoney = `The ${child} has no money left!` dollars > 0 ? console.log(discountDollars) : console.log(noMoney) } const giveMoneyToChild = child => { let dollars = 3 const discountDollars = () => { dollars -= 1 displayDollarsLeft(dollars, child) } return discountDollars } const boyBuysCandy = giveMoneyToChild('boy') const girlBuysCandy = giveMoneyToChild('girl') boyBuysCandy() // 'Discounted 1 dollar to the boy... 2 dollars left' boyBuysCandy() // 'Discounted 1 dollar to the boy... 1 dollar left' boyBuysCandy() // 'The boy has no money left!' girlBuysCandy() // 'Discounted 1 dollar to the girl... 2 dollars left' boyBuysCandy() // 'The boy has no money left!'
Both boyBuysCandy and girlBuysCandy took a "picture" of the giveMoneyToChild function's scope, therefore, two separate, independent closures were created.
The girl is patient, but the boy goes ahead and spends all his money right away. Only after that, the girl decides to spend her first dollar to buy a candy, and the boy tries to get another candy without paying for it, but he can't... Candies are not free!
That's all for this article, I hope you found it useful. See you in the next one!