JavaScript Functions At Run Time : Closures

JavaScript Functions At Run Time : Closures

Functions Retain Their Scope

It is important to recall that when an identifier (i.e., a variable) is used, the JavaScript engine will check the scope chain to retrieve the value for that identifier. The identifier might be found in the local scope (either in the function or block). If it's not found locally, then it might exist in an outer scope. It'll then keep checking the next outer scope followed by the next outer scope until it reaches the global scope (if necessary).

Identifier lookup and the scope chain are really powerful tools for a function to access identifiers in the code. In fact, this lets you do something really interesting: create a function now, package it up with some variables, and save it to run later. If you have five buttons on the screen, you could write five different click handler functions, or you could use the same code five times with different saved values.

Let's check out an example of a function retaining access to its scope. Consider the remember() function below:

function remember(number) {
    return function() {
        return number;
    }
}

const returnedFunction = remember(5);

console.log( returnedFunction() );
// 5

When the Javascript engine enters remember(), it creates a new execution scope that points back to the prior execution scope. This new scope includes a reference to the number parameter (an immutable Number with the value 5). When the engine reaches the inner function (a function expression), it attaches a link to the current execution scope.

This process of a function retaining access to its scope is called a closure. In this example, the inner function "closes over" number. A closure can capture any number of parameters and variables that it needs. MDN defines a closure as:

"the combination of a function and the lexical environment within which that function was declared." This definition might not make a lot of sense if you don't know what the words "lexical environment" mean. The ES5 spec refers to a lexical environment as:

"the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code."

In this case, the "lexical environment" refers the code as it was written in the JavaScript file. As such, a closure is:

The function itself, and the code (but more importantly, the scope chain of) where the function is declared When a function is declared, it locks onto the scope chain. You might think this is pretty straightforward since we just looked at that in the previous section. What's really interesting about a function, though, is that it will retain this scope chain -- even if it is invoked in a location other than where it was declared. This is all due to the closure!

So looking back at the above example -- after remember(5) is executed and returned, how is the returned function still able to access number's value (i.e., 5)? In this section, we'll investigate how closures allow us to store a snapshot of state at the time the function object is created.

Every function has its own closure just like every function has its own scope. What makes a function's closure powerful is when that function is returned in another function. When a function is declared in a function but returned and run outside of where it is declared, it will still have access to the scope it was declared in because of the closure.

Creating a Closure

Every time a function is defined, closure is created for that function. Every function has closure! This is because functions close over at least one other context along the scope chain: the global scope. However, the capabilities of closures really shine when working with a nested function (i.e., a function defined within another function).

Recall that a nested function has access to variables outside of it. From what we have learned about the scope chain, this includes the variables from the outer, enclosing function itself (i.e., the parent function)! These nested functions close over (i.e., capture) variables that aren't passed in as arguments nor defined locally, otherwise known as free variables.

As we saw with the remember() function earlier, it is important to note that a function maintains a reference to its parent's scope. If the reference to the function is still accessible, the scope persists!

Closures and Scope

Closures and scope are so closely related that you may even be surprised you had been working with them all along! Let's revisit an example from the previous section:

const myName = 'kendrick';

function welcomeMyself() {
  const you = 'student';

  function welcome() {
    console.log(`Hello, ${you}, I'm ${myName}!`);
  }

  return welcome();
}

welcomeMyself();
// 'Hello, student, I'm kendrick!'

To recap: myName is a variable defined outside a function, hence it's a global variable in the global scope. In other words, myName is available for all functions to use.

But let's look closely at the other variable: you. you is referenced by welcome(), even though it wasn't declared within welcome()! This is possible because a nested function's scope includes variables declared in the scope where the function is nested (i.e., variables from its parent function's scope, where the function is defined).

As it turns out, the welcome() function and its lexical environment form a closure. This way, welcome() has access to not only the global variable myName, but also the variable you, which was declared in the scope of its parent function,welcomeMyself().

Let's see closures more in action!

const number=3
function myFunction() {
    const otherNumber = 1
    function logger() {
       console.log(otherNumber)
    }
    return logger
}
const result = myFunction()
console.log(result)
// 1

From the above code, closure is applied by passing arguments implicitly.To break this down, we have a global variable number which just prints the number 3. We have a function called myFunction with a local variable called otherNumber which just prints a number 1 as well as a logger function that returns otherNumber. At the end, myFunction returns the logger function which is stored in a variable called result. This outputs 1. But how is this possible? The nested logger function is able to access the local variable declared in its parent function scope even after the parent function returned.

Again, consider the following code:

function myCounter(){
   let count = 0;
   return function (){
       count += 1;
       return count;
   }
}
const result = myCounter()

console.log(result)
// 1
console.log(result)
// 2
console.log(result)
// 3
console.log(myCounter.count)
// undefined
console.log(count)
// undefined

From the above code, a closure is defined through a function declaration that stores a snapshot of scope. To break this down, we have the myCounter function that uses a closure to create a private state. First, we have a local variable that is set to 0 then myCounter function returns a function that increments the count by 1 and then returns the count. A really cool thing here is that this function has a private but mutable state but cannot be accessed externally because a closure closes over the count variable over here. and hence, we cannot access the count variable externally. When we invoke our function and store it in a result variable, A log of the result variable shows that the initial value of the count displays but subsequesnt logs shows an increment. Since count keeps incrementing, we know that this data is retained and can also be modified

Its really good to use closures to create a private state and hence from the myFunction() above, as noted earlier, there is no way the count variable can be accessed outside of the function.

Conclusion

A given function (and its scope) does not end when the function is returned. Remember that functions in JavaScript retain access to the scope that they were created in! A closure refers to the combination of a function and the lexical environment in which that function was declared. Every time a function is defined, closure is created for that function. This is especially powerful in situations where a function is defined within another function, allowing the nested function to access variables outside of it. Functions also keep a link to its parent's scope even if the parent has returned. This prevents data in its parents from being garbage collected.