This is part of a series where I try to explain through each of 33 JS Concepts.
This part corresponds to the Scopes and Closures.
Scope in programming stands for visibility and usage. For reasons considered obvious (but ones that we will still discuss), we cannot allow all variables in our program to be global. Scope is what limits the variable to used across certain limits and boundaries.
We cannot declare two variables with the same name. This would throw a reference error.
var length = 1;
// some operation going on, after sometime you think
var length = 1; // Nope!`}
But we absolutely cannot not use the same variables for different purposes. If there are no boundaries, it becomes confusing for people who read your code. This becomes more problematic when there are lots of people on the team. How does anyone know if someone else has already declared the variable?
Clear boundaries on where you can access a variable makes it easier for all developers in the project to avoid conflcits.
When we complete the usage of a variable, we want the variable and the data it is holding to be garbage collected. In dynamic languages, we expect this to happen automatically. But if we don't have any boundaries on where the variable can be accessed it would happen that the compiler does not have any hint on when to collect the garbage. Except may be at the end.
Having clear boundaries on where variables can be accessed can tell the compiler, that at the end of this scope it is safe to garbage collect these variables.
If all our variables are global, it means that anyone can change them.
For eg. in one of the maintanence projects I had to make a Custom Event to broadcast an event to another part of the application itself. But whatever I did, I could not get the custom event to fire. What happened was that someone else had already declared a class named CustomEvent
(they wanted to customise the Event
class, so be it!) on the global window and it was overriding my interpretation. Except the other person (git blame) who did it, did not even know that a function named CustomEvent
actually existed in JavaScript.
Imagine this happening to your variables, randomly at runtime. This is what happens if we do have some sort off ownership for the variables and functions that we write.
JavaScript has two kinds of scope:
We will talk about function scope first.
Function Scope means that any variable declared would be available inside the function. This has existed and was widely used from olden times in JavaScript.
function action() {
var a = 2;
... // actions
// a can be accessed anywhere in this function.
}
Wherever you declare a variable, JavaScript would proceed to hoist these upto the top of their scope. But let us be semantically correct here. JavaScript does not move the variables physically all your code remains the same. The compiler just picks the variables in it's current scope and declares them in compiler time with the value undefined
.
This is how you can access a variable before declaration and get undefined
as an answer.
You don't have to be afraid of hoisting. In fact hositing is what helps you when declaring functions in whichever order you want. Since the compiler would hoist it to the top anyway, it does not matter which order you declare it in. But with variables declared with var
, it is important to write short precise functions.
Also, note that only declarations are hoisted, they would not take any value if used before initialisation.
This is widely used from ES6. Block refers to a {}
in a program.
Block Scope means that the variables defined inside a {}
can only be used inside it. Variables can be put in block scope by declaring them with let
or const
.
Note that functions also form block scope.
function action(limit) {
const a = 10; // a can only be used inside this function
if (a < limit) {
const b = a + 2; // b can only be used inside this if block, a can also be used here as this block is inside the execution context of a's block
return b;
}
}
Does hoisting happen in Block Scope? Yes. But if we try and use a variable before it's actual declaration, we get a ReferenceError
.
That does not make any sense. If they are hoisted, shouldn't they be undefined?
To get ahead of this question, JavaScript spec defines something known as a Temperal Dead Zone (TDZ). This is the difference of when it is memory (remember: Hoisting is just compiler putting variable declarations in memory) and it's actual declaration in code. When a block scoped variable in TDZ is accessed, it throws a ReferenceError
.
JavaScript is a language that treats functions as first class citizens. This is a part and parcel of the functional languages. Functions are just objects in JavaScript and they can be assigned to variables, pass them to functions or return them from functions itself.
Let's look at these conditions one by one:
const foo = function (a) {
return a++;
};
Here function is assigned to variable foo
, to invoke this function we have call foo()
. foo
here is a reference to the function and can be reassigned or assigned to some other variable.
We just saw that functions can be assigned to variables. This is essentially an easy by product of the same. You can pass these references around as you would do with any other object.
Here, you can see that inc
is being passed into the counter
function which in turn is invoking it. You might wonder why we have to take this approach instead of just calling inc
directly from counter
. The difference is that now we can control the factor of how much the counter
is going to increment by from the outside. That is, we can pass another function that increments by 2 and Boom! we have a counter that adds by a factor of 2 instead of 1. We can do this without changing the function at all.
We are not invoking it when write
inc
, the incrementing is only when the brackets are attached to it.
This is going to be longer than the other ones, but bear with me here.
With the last example, we discussed how we can change the counter
function by passing in different functions. Let us look at how we might achieve that result:
We just created two functions: inc
and incBy2
. The first function increments by 1 and second increments by 2. But I guess we can agree that this is not the most elegant approach. If we had to create a function that adds by 3, then we would require a third function. How can we create a single function for this purpose?
Let us look at the simplest approach first:
Well, this does the work. But this is breaking the expectation we had set for ourselves. The whole objective of passing a funtion to counter
was the fact that counter
did not need to know the factor which was being increment or any operation being performed. By passing factor
into counter
, we have broken that encapsulation. We need better ways to create dynamic functions that we can pass into counter
.
If this seems natural for you, then Congrats! 🎉🍾 You have successfully understood closures. If it doesn't read on:
createInc
is a function that returns a function, let that sync in; A function that returns a function.
What we have to be concerned here is the variable factor
that is passed in. If you look of the call stack of this program, you can see that createInc
is added to the stack and is popped as soon as the function inside it is returned. But the returned function is still using factor
in runtime. How is that retained?
When a function is created, the function stores both it's local function and the context the function was created in. This context is known as the closure environment. A function when it is created, it stores the local variables and the closure scope it was created in. This closure scope is garbage collected only when the function itself is collected. This is part execution context of the function.
Well, it should. Scopes and Closures are some of the most integral cornerstones of the language. It can and should influence the way you think about the language and declarations.
Is there something I missed? Something wrong? Something good? Ping me on Twitter