Master the internal mechanics of JavaScript and build an engineering grade mental model of how the language engine actually functions. This comprehensive course covers advanced conceptual topics ranging from execution contexts and closures to the complexities of multi-threading in Node.js. Whether you're preparing for a senior engineering interview or architecting high-scale applications, this roadmap will bridge the gap between simply writing code and truly engineering systems. Sumit Saha created this course. Most developers learn JavaScript by memorizing syntax, but the best engineers understand the engine. Welcome to thinking in a JavaScript way. In this comprehensive course, we are moving beyond the surface. We'll start by mastering the invisible boundaries of scope and closures before peeling back the curtain on the execution context and hoisting to see how the engine actually thinks. We'll bridge the gap between functional logic and object-oriented programming through prototypes and master the browser's pulse with even delegation and propagation. Finally, we'll scale into high-performance territory covering asynchrony, memoization, and the complex world of multi-threading in both the browser and Node.js. Whether you are a junior developer hit by weird bugs or a senior engineer looking to solidify your foundation for high-level architecture, this course was built for you. By the end of this journey, you won't just writing JavaScript, you'll be thinking in it. Let's dive in. Suppose I define a variable var x is equal to 23. Then I print it in the console using console.log. After saving the file, notice that the browser console shows 23. Perfect. Now, the variable x I defined here is declared openly, meaning it's not inside any function. It's defined right at the root of the program. Next, let's say I create a function named my funk. Inside that function, I write var y = 10. This variable y exists only within the my funk function. It doesn't exist in the root. Think of it this way. The root contains the function, and inside that function lives the variable y. Clear? Now, when we talk about this my funk function, everything inside its curly brackets belongs to the world of my funk. Anything outside that, the root level, is the parent's world. So, you can imagine it like this. My funk is a child, and the root is the parent. We know that when we define a variable globally in the browser, it actually becomes a part of the window object. For example, earlier we defined x = 23. When we did that, it automatically got attached to the window object. If we now write window.x inside console.log, and then save the file, you'll see in the browser that it prints 23, right? That means the variable x is actually a property of the window object. But, the interesting part is we don't need to write the word window every time. The browser automatically understands it because it knows that x is a variable from the global scope. Now, notice something. So far, we have printed x inside the parent scope. This time, let's also print it inside our function. So, inside my funk, let's write console.log printing x from my funk. Then, we call the my funk function. After saving and running it in the browser, you will see it prints printing 23 from my funk. That means I can access the parent variable from inside the child. In other words, from inside the function. This might sound very normal, but in the world of web development, it's not quite the same everywhere. Because in web development, there are many different programming languages. Some developers use JavaScript, while others use PHP, .NET, or Ruby to build web applications. So, when developers come to JavaScript from another language, they often find this behavior weird. Because in most other languages like PHP, you can't do this. You can't simply define a variable globally or outside a function and then access it freely from within a function. That's exactly why many people think JavaScript is a weird or even a toy language. The truth is, JavaScript is quite liberal when it comes to variable sharing. Whereas, most other languages are much stricter about it. To make this concept easier to remember, let's think of a simple real-life example. When we were kids, our parents used to buy us chocolates or toys. We would play with them or eat them freely. No one stopped us. In other words, whatever the parent brought, the child could use. But when we grew older and started buying things with our own money, those things were only ours. Our parents didn't use them. The same thing happens in JavaScript. A child can use the parent's belongings, but the parent cannot use what belongs to the child. Throughout this lesson, we have been using the word world. In JavaScript, this world is called a scope. That means if we create a function, the area inside its two curly braces is the function's own world. We call it is the function scope. And the outermost level, the root or the parent, is called the global scope. Some people also call it the window scope. Clear? That's the basic definition of scope. Now, let's see a few implementations to understand why scope is such an important concept. Earlier inside our myFunc function, we defined Y is equal to 10, right? Now, instead of printing X inside our console log, let's replace X with Y. So, we will update the code to print Y instead. Then, outside the my func function, in the global scope, we will again write console.log Y to print it. All right. Now, save the file and check the browser console. You will notice that Y prints once successfully, but the second time it shows an error. The error says reference error. A reference error means that JavaScript couldn't find the variable Y. That's because we defined Y inside the function scope. So, we can use Y inside that function, but not outside it. Not in the global scope. It's just like the parent and child example, right? The child can use what the parent gives, but the parent can't use what belongs only to the child. Let's look at another example. Earlier in the parent scope, we defined var X is equal to 10, right? Now, inside our my func function, instead of writing var Y is equal to 10, let's replace that with X is equal to 10. Also, update the console.log inside the function. Replace Y with X. Then, update the global console.log as well to print X. Now, save the file and check the browser console. You will notice that the value of X has changed. That means the child scope, our function, can also modify values that belong to the parent scope. Now, let's see something even more interesting. If we add the var keyword before X inside the my func function, so it becomes var X is equal to 10, and then save the file again, and look at the browser console, here's what happens. Inside the my func function, the output for X shows 10, but the global console.log still shows 23. What's happening here is this. If I write X is equal to 10 without using var, JavaScript assumes I am modifying the global X. But if I use var X is equal to 10 inside the function, JavaScript treats it as a completely new variable that exists only inside that function's scope. However, directly modifying a global variable from inside a function can be very risky, especially in large code bases. It can cause variable name conflicts and data mismatches that are hard to debug later. So, this is how scopes work in JavaScript. Now, scopes in JavaScript are mainly of three types. The first one is global scope. Any defined in the root or global level can be accessed from any inner scope, including functions. That's what we call the global scope. The second one is function scope. Any variable defined inside a function can only be used within that specific function. If you try to access it from outside, JavaScript will throw a reference error. And finally, we have block scope. Let's see an example of that. Inside our my func function, we will define an if block. So, we write if true. Since we have given true as the condition, that means every time my func is called, this logic will always be true and the program will enter the block. Now, inside this if block, we will write var a is equal to five. And on the next line, let b is equal to 10. So, here we have created two variables, one with var and the other with let. Next, outside the if block, we will try to print both of them using console.log a and console.log b. To make the output clearer, we will remove all the other console.logs from the code so that only a and b get printed. Now, save the file and check the browser console. You will notice that the value of a prints correctly, but when it comes to b, it throws a reference error. And the reason behind that is block scope. Variables declared with var belong to the function scope, meaning they can be accessed from anywhere inside the entire function. But, variables declared with let or const are limited to the block scope. That means they can only be accessed inside the curly braces block where they were defined. So, the variable var a can be accessed throughout the entire function, while let b is visible only inside the if block. If we try to access it from outside, JavaScript throws a reference error because once we step out of that block, JavaScript can no longer find that variable. That's why we say variables declared with let or const are block scoped, whereas variables declared with var are function scoped. If we had printed b inside the if block using console.log b, it would have worked perfectly because variables declared with let or const are only accessible within their block scope. I hope this explanation helped you clearly understand the concept of scope. To summarize everything, scope is simply the area where a variable can be accessed. A child can access the parent's variable, but a parent cannot access the child's variable. And in the browser, the root object of the global scope is window, while in Node.js, it's global. I believe after today's discussion, you will no longer be confused about how scope works in JavaScript. Still, if you find anything unclear, feel free to ask in the comments. In the next video, we'll dive into some more of JavaScript's weird but beautiful concepts like closures, hoisting, and the execution context. Thanks for watching everyone. Hi everyone. I hope everyone is doing well. Today's tutorial is about a topic in JavaScript that many developers find a bit intimidating, closures. In my nearly 17 years of programming experience, I have noticed that just hearing the word closure makes a lot of JavaScript developers nervous, almost instinctively. So, the main goal of this tutorial is to remove that fear. By the end of this video, you should be able to confidently say, "No more fear of closures." Closures aren't actually complicated at all. We often make them seem hard just because we don't understand them clearly. Many tutorials don't explain the topic deeply, which leaves us confused asking questions like, "What exactly is closure?" or "What does it really mean?" In this video, I will walk you through several examples step by step, so it might be a bit long, but if you stick with me I promise all your confusion about closures will disappear. Let's get started. Usually, when we want to use an outside variable inside a function, we pass it as a parameter. For example, let's say we write a function called sum. Here, two numbers are taken as parameters. Now, if we want to add these numbers and return the result, we write return num1 + num2. If we want to see the output, we write console.log and pass sum (2, 3). The output will be five. Simple, right? But in JavaScript, there's a way to do the same thing without passing parameters. How? Now, let's remove the parameters from the sum function. Then, outside the function, in the global scope, we define two variables, num1 and num2. Now, even if we call sum function, the output will still be five. So, the question is, how does JavaScript do this? It seems a bit strange, right? Inside a function, we are using variables that don't actually belong to that function. They exist outside the wider environment where the function was created. In simple terms, the function is using variables from its parent scope. We did a video on scope before. This is actually one of the most fundamental concepts in JavaScript. Everything from a parent is accessible to the child. And if there were nested functions inside this function, even the deepest child function could access variables like num1 and num2 from the parent. In other words, the parent's variables are fully accessible to the child, but nothing from the child can be accessed by the parent. For example, if we define a new variable inside the sum function, it cannot be accessed from outside the function. We covered this in detail in our previous scope video. So, this is the core idea of scope. We said that the system of scope is theoretically called lexical scoping. Now, you might wonder, since today's topic is closure, why are we discussing scope so much? It's because closures and scope are deeply connected. Understanding scope is essential before we can truly grasp closures. Now, according to lexical scoping, a child function can access its parent's variables, but a parent cannot access the child's. This isn't random. It's a specific convention or guideline in JavaScript. But, how does this actually work under the hood? Imagine we're solving a problem. That's when closures come into play. Understanding closures is crucial for situations like this. The word closure literally means a bond or enclosure. Think of it like keeping a variable safely locked away, just like storing something inside a box. Even though the box is closed, we can still use its contents when needed. This is why it's called a closure. Because we keep a function's variables enclosed in such a way that even if the outside world cannot access them directly, the function itself can still use them whenever required. To understand closures properly, we need to remember one more small point. In JavaScript, whenever we write a function, the function is actually treated as an object. Yes, every function in JavaScript works as an object. Just as we can console log an object to see it, we can also console log a function to inspect it. Now, let's print our sum function. For that, we write console.dir and pass the sum function body. Notice that we are not calling the function. We are just printing its body. We use dir instead of log because console.log only shows the function body, while console.dir displays the function as an object, letting us see each of its properties one by one. You can think of it as an upgraded version of console.log. Looking at the output, we will see an object. Expanding it reveals many properties, like name, length, prototype, and more. At the very bottom, there's a property called scopes. Inside scopes, there's a section named global containing even more details. Today, we will mainly focus on this scopes property. Now, notice something. The sum function has its own world, right? So, why is it still referencing the global scope? Every function actually maintains a connection with its parent environment. It doesn't just live in its own world. It keeps a link to the environment where it was created. Simply put, it always holds a reference to its parent. Why does it do this? Because if anything changes in the parent environment, like a variable's value or the need to use it inside the function, the function can still access it. This whole process is the core concept of a closure. A function keeps track of the variables it uses from outside its own scope by closing over its parent and its parent's parent. Essentially, the entire scope chain above it, holding them as references. That's why we said this example is actually the simplest form of a closure. The sum function itself is a closure because it has captured some variables from its outer environment and can use them whenever needed. Even though we often see closures explained with examples where a function is inside another function, the fundamental idea starts here. Any function that retains access to variables from its outer scope is, in essence, a closure. We can understand this even more clearly if we tweak the previous sum function a bit. In the earlier example, we directly wrote return num1 + num2, right? This time, instead of doing that, we will have the sum function return another function. Return function. And inside this inner function, we write return num1 + num2. So, what does this mean? The sum function is no longer returning a value directly. Instead, it's returning a function. Now, let's create another variable called my function. var myFunc is equal to sum. What happens here? We call the sum function and whatever it returned, the inner function is now stored in my function. In other words, myFunc is essentially the inner function returned by sum. Make sense, right? If we now print myFunc to the console, console.dir myFunc, we will see num1 and num2 listed as variables in the output. What does this mean? This function is still holding onto global environment. Even though it's an inner function, it's still connected to the global scope and maintains the same global references as before. Now, let's do one thing. Remove the num2 variable from the global scope and define it inside the sum function instead. Now, let's look at the output again. This time in the browser, we can clearly see something labeled closure. In other words, the browser is directly showing that a closure has been created inside this function. In older versions of Chrome, even in the previous example, you would have seen closure here. But in the newer versions, it shows as global until a function actually closes over another function. So, whenever a function is returned from within another function, that's when the browser displays closure. However, keep in mind that theoretically, the previous example was also a kind of closure. The difference is just in how the browser presents it. When we did console.dir myFunc, we saw that this inner function is using both num1 and num2. Where is num1? In the global scope. And num2? Inside the sum function scope. So, what is this inner function doing? It's taking a reference to num1 from the global scope and at the same time taking a reference to num2 from its parent function, sum. In other words, this inner function now carries two worlds within it. One is the global scope and the other is its parent scope. This is exactly what a closure does. It keeps all the outer scopes it needs enclosed so it can use their variables whenever necessary. In the browser, we can see that inside this closure, num2 exists while num1 remains in the global scope. So, num1 is no longer part of the closure. What does this mean? The function only carries the parts of the environment it actually needs for its execution. Simply put, it takes all the variables it needs along with their references as a compact package. Think of it like the function holding onto references. Whenever any of these variables are updated, the function can see those changes because it's still connected to the same references. So, let's say we called myFunc is equal to sum once. Now, if we keep calling sum function repeatedly, there's no problem. Each time a new function will create its own separate scope and keep a reference to that scope. In simple terms, we defined a function and then called it elsewhere. Every time we call that function, it can still access the data from its previous scope. That's because every function preserves all the information from its parent scope as references. This is exactly how a function remembers its outer variables. This is what a closure is. So, as far as we understand, this is the core idea of a closure. Hopefully, it's clear to everyone now. Now, we move to the next step. This will be a bit more advanced. I will explain very slowly, so it might take the video a little longer. Closures are really important, and my goal is that after watching this video, no one will be afraid of the word closure. Those who watch from start to finish should have a complete understanding of closures. There's nothing to fear about closures. They're actually very simple. Imagine a function. If it wants to use a variable or data that exists outside its own world, it brings the parent scope of those variables along with it. In other words, it closes over the entire parent scope where those variables exist. This is the concept of a closure. Hopefully, it's crystal clear now. Next, we'll look at several examples one by one to understand the concept more deeply. We'll also go through different use cases, so it becomes easier to grasp. So, let's get started. Think about other programming languages. When you want to create a private property, what do you usually do? In other programming languages, we define a property inside a class and mark it as private, so that no one can access it directly from outside the class. Then, inside the class, we create one or more public functions like getters or setters, which allow controlled access to or modification of that property. In other words, you can't touch the property directly from outside. You can only interact with it through specific functions defined inside the class. In JavaScript, we can achieve the same idea much more simply using closures entirely in a functional style. How? Let's see an example. Suppose we have a simple function, function bank account and pass initial balance. We have named it bank account and it takes the user's initial balance as a parameter. Inside it, we define a variable, var balance is equal to initial balance. So, the user's initial balance is stored internally in a variable called balance. Next, we return a function that returns this balance variable. In other words, the balance can only be accessed through this return function. Outside, in the global scope, we create a variable, var account is equal to bank account and pass 150s. Here, we call the bank account function and pass 150 as the initial balance. What is this function actually doing? It's returning another function. So, now the account variable holds that return function. If we write in the console, console.log and pass the account function call, the output will be 150. But, if we try console.log and pass balance, it won't work because the balance variable cannot be accessed from outside. What does this mean? The balance property is protected or private. No one from the outside can directly touch it. To see the balance, you can only call the return function inside the original function. This is how we keep balance as a private variable and control access to it. So, what's the role of the closure here? It's exactly what makes this possible. The inner function is the closure. If we write console.dir and pass account, we'll see that inside the account function is the return function. Expanding it, we'll see that the balance variable exists inside the closure. Exactly, right? This means that balance wasn't created inside the account function itself. It was created one scope level up. Yet, we can still access balance from the return function. It's similar to the previous example, but this use case is slightly different. Here, we are showing how a private property can be kept secure. Even though we call the inner function from the outside, it still had access to balance within its scope. So, the outer scope cannot directly access balance, but thanks to the closure, we can maintain a reference to it. Even though the function is called from outside, the closure allows us to access the private property in a protected way. Why protected? Because we are not accessing it directly. We can only see balance through the function call. This is another powerful use case of closures. Securing private properties so that they cannot be directly accessed from outside, but only through specific functions. So, this is one of the powerful features of closures. Now, let's look at another aspect of closures. Let's revisit our first example. In that example, we used a variable called num2 inside the inner function. That's why the function acted as a closure. Now, let's keep the variable, but stop using num2 inside the inner function. If we check the output, we'll see that the closure is gone. Do you see why? It's because num2 isn't used inside the inner function. JavaScript smartly recognizes that this variable isn't needed, so it's not included in the closure. In other words, JavaScript decides on its own variables that won't be used inside the function, even if they exist in the outer scope, aren't included in the closure. Only the variables that the function actually needs become part of the closure. For example, if we define another variable inside the sum function, var num is equal to six, and do nothing else, there's still no need for a closure. But, if we modify the inner function to return num instead of num one, what happens? You'll see the closure appears again. This time the closure contains only num. Num two won't be there, but num one remains because it exists in the global scope. JavaScript preserves this scope to maintain lexical scoping. For instance, if we look at the global scope, we can still see num one. Right? That's because we used the var keyword. If we had used let, it wouldn't be visible. We will discuss this issue later to avoid confusion. The key difference we are noticing is num one exists in the global scope, so it remains in the closure's environment, but if a variable inside the inner function isn't used, it's not included in the closure. For example, here, if we use num one, we are accessing the global variable. So, what happens now? Will there be a closure? Look, there isn't one. Since num one exists in the global scope, there's no extra need. The global scope is sufficient, and no separate closure is required. This shows how closures actually work. A closure decides which variables need to be kept inside the function, and which don't. JavaScript makes this decision automatically. We just need to remember lexical scoping, so that outer scope variables can be accessed. In simple terms, closures make intelligent decisions. Variables used inside the function are kept inside. Variables in outer scopes that aren't used aren't included, and global variables are accessible directly. So, there's no need to include them in the closure. So, here we kept num1, and it exists in the global scope. The function can access it directly from there, but if the inner function used only num, which doesn't exist in its own scope or globally, then a closure would have to be created to carry that variable along. In short, a closure doesn't wrap everything. It doesn't include variables that are already outside. This is an important point. Another important point is that global scope variables are never included in closures. Now, let's move on. Often, there's confusion about what a closure appears and when it just shows global. Even senior interviewers sometimes hesitated to call the global scope a closure at first glance. So, what's the real point here? Let's break it down. If we look at JavaScript documentation, Mozilla maintains it. I visited their site and checked the 2016 documentation before the current definition. According to that old documentation, the definition of a closure back in 2016 highlighted something important. In the brackets, it said, "Variables that are used locally, but defined in an enclosing scope." This means variables that are used locally inside a function, but are actually defined in an outer scope. This is the key point. In our previous examples, this is exactly what was happening. Only variables that are actually used inside a function are included in the closure. Variables that exist in an outer scope, but aren't used inside the function, aren't part of the closure. In simple terms, a closure is a function that remembers its local scope along with the necessary variables from an outer scope. So, even if the function is called from outside, it can still access those variables. Now, let's go back to our example. Suppose we have a variable num defined outside, but we use it locally inside the function. That's why the browser shows it as a closure, just like the documentation says. But, if we don't use the outer variable inside the return function, like before when we did return num1 + num2 directly, what happens? We removed everything else and just return num1 + num2. If we call the sum function directly, it works fine. But, if we did console.dir sum, what do we see? The word closure doesn't appear. Why? Because num2 is local inside the function, and there's no need to include it in a closure. A closure is only needed to use variables from an outer scope. Since this is the very first level outer scope, meaning the global scope, and it's not inside any function, the sum function is already capturing it in its own scope. So, no additional closure is created. Now, the question is, when does the browser show a closure and when doesn't it? That's the critical point. The explanation comes straight from the documentation. Variables that are used locally, but defined in an enclosing scope. Look, our num1 variable is defined outside, but used locally inside. However, num1 doesn't have any enclosing scope. Here, an enclosing scope means a scope that wraps around another scope. For example, inside a set of brackets. But, num1 is directly in the global scope, not inside any enclosing scope. If we want to bring this into an enclosing scope, we need to wrap the whole thing inside a function. We can write an anonymous function like this. Then we put everything inside that function and immediately call it. This is known as an immediately invoked function expression or IIFE for short. Basically, it's a function that gets defined and executed at the same time. When we use an IIFE, everything gets moved into an enclosing scope. The num1 that was previously open in the global scope is now inside this function. So, it's now part of an enclosing scope. If we check in the browser and expand it, everything looks the same as before, but now we see the word closure, right? That's because we have brought the variables into an enclosing scope. So, the browser now shows it as a closure. To clarify, if someone ever gets confused or disagrees, they might say the outer one isn't a closure. It's just a global scope. But theoretically, we can still consider it a closure. Some might insist that's a closure, while others may not agree. For those who don't, this explanation helps. The thing is, JavaScript always keeps the global scope intact to maintain lexical scoping. People who disagree with me might say the global scope is just preserved. That's not a closure. And that's fair. But the concept is really the same. If a variable from an outer scope is used or referenced inside the function, it behaves just like a closure. The idea is consistent. There's a small difference, though. The global scope keeps all variables that exist outside any function. That's why some might argue it's not technically a closure. But from a theoretical perspective, it behaves like one with only minor differences for global variables. If we hadn't done this and went back to the previous setup, the browser would no longer show it as a closure. Look, now the browser doesn't show it. And the var num1 is in the global scope, right? So, num1 is being used here. Now, if we add another variable below it, like var num3 is equal to five, this num3 isn't used by the sum function. But, if we look in the global scope, we can still see it. The browser shows num3 as well. But, as we saw earlier, a closure only keeps what's necessary. Here, num3 exists because it's part of the global scope. The global object always holds references to its own variables, which is why num3 is feasible. This often causes confusion. Should we call it a closure or not? My point is, in the 2016 documentation, the term enclosing scope was clearly mentioned. In the current documentation, that phrase is missing. This means they have intentionally avoided that confusion. The modern definition now says, "Closure is the combination of a function bundled together, which is a written in a more complex way compared to before." In the updated definition, it says, "Closure is the combination of a function bundled together with references to its surrounding state." Here, state refers to the lexical environment. That could be the child's environment, the parent's environment, or the entire scope. Based on this definition, it means a function keeps itself along with any variables it needs to remember, all bundled together. Explaining this clearly in words can be tricky, and I might not fully capture it right now. But, we will see some examples ahead, which will make each use case clear. We will show that mistakes can happen while we use closures, why understanding them is important, and why closures are asked about repeatedly in interviews. While following this tutorial and seeing more examples, everything should become much clearer. Now, let's look at another aspect. We are going back to the previous IIFE function. Here, we have a function called sum that adds num1 and num2 and returns the result. Both num1 and num2 exist inside the IIFE function. This means we have kept the whole set up self-contained within a closure function. Now, when we call the sum function, console.log and pass the sum call, and on the next line write console.dir sum, let's check the output. Initially, 2 + 3 gives 5, which is exactly what we see. Since num1 and num2 now exist inside the global scope of the IIFE, we can modify these variables if we want. num1 is equal to 6, num2 is equal to 7. Then, if we call again console.log sum and console.dir sum, we'll see two different results. The first call returns 5 because initially 2 + 3 was calculated. After changing num1 and num2, the next call returns 13. So, we can see that a closure keeps hold of the outer variables and makes them accessible inside the function. Now, at that moment we check the function using console.dir, remember? First, we expand the dir at the bottom. Here, we see an entry labeled as closure and inside it we notice num1 is equal to 6 and num2 is equal to 7. Great, because before writing dir, we had changed the values of num1 and num2. So, it's showing the latest values. But, if we go back to the previous state and expand the first console.dir, surprisingly, it still shows num1 is equal to 6 and num2 is equal to 7. Both are the same. Pretty weird, isn't it? That's because when we did console.log, the result showed 2 + 3, but in the very next line, the values hadn't actually changed yet. In the a we see that inside the closure, the values remain six and seven. This is exactly what we have been emphasizing. A closure doesn't really hold the values themselves. It holds a reference to the variables. So, what does this mean? It means that there's a pointer stored to the memory location of our variable. Once a reference is stored, the pointer itself stays the same, but the value can change anytime. When we use console.dir, the browser is showing that reference, which is why it always displays the latest values. The browser works very fast, and the reference has already been updated. When we set num1 and num2 to six and seven inside the closure, the reference gets updated. We are seeing the exact same variable, but we don't see the intermediate values. However, when we do console.log, the function uses its corresponding values correctly. That's why not every change in the intermediate scope is clearly visible. Because of reference updates, we always see the latest value, not the direct intermediate state. So, here's an important point to understand. A closure doesn't store the actual values inside. It stores references to those values. Keeping this concept in mind is really crucial. Now, let's look at another aspect of closures. Let's go back to the previous example. Earlier, we declared two variables in the global scope, num1 and num2. So far, we have been using the var keyword. So, the output will reflect that accordingly. Now, if we don't put anything inside an IIFE and just stay in the global scope, we won't see any closure in the browser. But, where do num1 and num2 live? Obviously, in the global scope. That's why we write console.dir sum. In the browser, we can see num1 and num2 inside the global object. Now, here's the interesting part. What happens if we replace these var keywords with let? Let's check that out. This is where the difference between var and let comes in. Many people think they're the same, but in JavaScript var and let are not equal. Simply put, var is the old JavaScript declaration and it's function scoped. That means a variable declared with var only lives inside the function it's defined in. If it's defined outside any function, it goes to the global scope. On the other hand, let is the new ES6 declaration and it's block scoped. That means a variable declared with let only exists within the block or scope where it was defined and cannot be accessed from outside. One important difference is hoisting. Variables declared with var are hoisted, meaning JavaScript moves the declaration to the top, but the initialization doesn't happen. So, if you use a var before it's declared, you will get undefined. With let, even though it's hoisted, the temporal dead zone ensures that using it before declaration throws an error. So, let's see what happens if we declare the variable with let. I have already provided a detailed tutorial on hoisting in this channel. So, please check that out for more information. The link to that video is in the description. So, let's see what happens if we declare the variable with let. Last time, when it was just in the global scope, we could see the variables when we did console.dir. Now, whenever we see a new object named script has appeared and num one and num two are no longer in the global scope. Why is that? It's because of the difference between let and var that we talked about earlier. Let is the block scoped while var is function scoped. If we treat the outer context as a main function, a variable declared with var becomes part of the global scope. But with let, it stays within its block scope and doesn't directly become part of the global object. So, let actually lives inside a separate object called script and not in the global scope. Understanding this is really really important because if you follow the tutorial and try printing variables while using let out of habit, the output won't be like it is with var. This can definitely be confusing. Simply put, let doesn't go into the global object. It exists inside a separate script object. But what if we wrap the whole thing in an enclosing function again, like before with an IIFE, where a self-invoking function calls itself? Let's copy this code and paste it inside an IIFE. When we check the output now, everything goes back inside its closure. Ultimately, the concept of closures remain the same. What changes is whether the variable goes into var or let's scope. Now, the situation becomes a bit simpler. We have a function and it's in its enclosing state. According to the definition of a closure, the inner function of this function is using the outer variable name one. So, this inner function definitely needs a closure. That closure comes from exactly this function. JavaScript creates the closure and packages name one inside it. The outer global world always exists separately, of course. Also, remember when using console.dir in the browser, the output will look different depending on whether you are dealing with let or var. So far, all the examples we have seen were very simple. Now, let's look at a practical example that will help you understand closures better and clear up any confusion. Let's define a function. Function stopwatch. We have named it stopwatch and it will work just like a real stopwatch. Like when we start a stopwatch, wait for a while, and then stop it to get the lapse time. This function will do the same. First, we write var start time is equal to Date.now. This stores the current time in start time. Then, inside the function we define get delay is equal to function. We create a get delay function, which will log the elapsed time to the console. For that, we write console.log Date.now minus start time. Here, the elapsed time is calculated by subtracting start time from the current time. Finally, we simply return this get delay function. The stopwatch function does just one thing. When you call stopwatch, it starts a stopwatch using Date.now and returns a get delay function. When you call that get delay function, it shows the elapsed time from the start time to the current moment. So, that's the entire purpose of the function. Now, let's call it. We write var timer is equal to stopwatch. Here, we have started the stopwatch. This function executes, which means start time is set, and the get delay function is defined. The stopwatch function itself isn't called directly afterward. We simply called it once, and the outer function returns the get delay function. We stored this returned function in timer. Now, on the next line, we'll create a fake delay. At this point, the stopwatch is already running because we called it, but we haven't printed anything from get delay yet. Before calling get delay, we create a fake delay like this. For let I is equal to zero, I less than 10 crore, I plus plus. Here, we use a large for loop to waste some time. We chose a big number intentionally, so it takes a noticeable delay. If we want, we can also perform some expensive operations in the loop, like calculating a random number. Math.random into a huge number. This way, we create a fake delay. Now, let's come to timer. Timer is actually a function because it was returned by calling stopwatch. This returned get delay function acts as our actual timer. Now, let's call timer and see what happens. The output doesn't appear instantly. It takes a moment and then it shows up. So, we get a delay. The question is, how is this still working? The stopwatch function where we initially called it has already finished executing. That means everything inside that function should be gone. So, how does timer still know the value of start time? Especially after we added such a long delay before calling it. This means the timer function still remembers its parent function, especially the start time variable inside stopwatch. It holds onto that reference. How? Because of a closure. When get delay was created, a closure was also created inside it that kept track of the start time variable. So, even after the delay and even after a long time, it can still use that old value. This shows that JavaScript is really smart. It tracks all variables, references, and closures, and when needed, it can use them again. Here, we are just demonstrating how closures work. The rest is handled automatically by JavaScript. It knows when something is needed and makes sure it works. This is why this behavior is possible, because of closures. If we do console.dir timer like before and check the output in the browser, you will notice it takes some time to appear because of the delay. But even after the delay, it still remains start time inside the closure. If we try console.log start time, we won't be able to access start time directly. But since timer is a member function, it can use that start time we initialized long ago before the for loop. It still remembers start time, no matter how long the delay. It can keep track. Even if there were more lines of code or more expensive operations during the delay, at the end of the day, when we call the timer, the closure ensures that start time is correctly preserved. This is one of the fascinating aspects of JavaScript. It can really remember such information, and this is one of the biggest use cases of closures. Because of closures, no matter how many times we call the timer function, each call works independently and keeps its own start time reference. Every time a new reference is created and held as long as it's needed. This is a small example, but in a large application, there could be countless closures holding references to many variables. Naturally, the question arises, if so many things are being remembered, will performance suffer? This is where JavaScript's performance optimization comes in. JavaScript is a smart garbage-collected language. That means when JavaScript realizes that a reference or variable is no longer needed, it automatically removes it from memory. In some situations, programmers can manually optimize. For example, in our case, we created a timer holding a reference to the get delay function, but we haven't called get delay yet. JavaScript doesn't know if it will be used in the future, so it keeps the reference. If we are certain it won't be used anymore, we can manually clear the reference by writing timer is equal to null. Now, timer won't work because we set it to null. JavaScript understands that it's no longer needed and garbage collects the reference from memory. If you try this in the browser, you'll see an error, type error, timer is not a function, because timer is now null. Simply put, setting timer is equal to null tells JavaScript this variable won't be used anymore. Forget it. The garbage collector then recognizes there are no references and quietly removes it from memory, preventing memory wastage. The interesting part is that JavaScript doesn't just run the code. It predicts a lot even before compilation. When it sees timer is equal to null, it already knows, "Okay, the programmer used this timer only this much and it won't be needed anymore." So, as soon as the code finishes running, it intelligently cleans up the memory. This makes our program automatically optimized. There are no memory leaks or browser load decreases and JavaScript executes faster. This is a very small example, but it already shows how elegantly you can manage performance in JavaScript. So far, all the examples we have seen were using closures in a synchronous way. Now, many people might wonder, "Okay, but how do closures work in asynchronous situations?" That's a very good question. In real-world coding, most tasks run asynchronously, like with set timeout, fetch, or event listener functions. The key point is synchronous code executes line by line, but asynchronous code takes some time to complete. That means you call a function, but its result arrives a little later. So, the question is, "If the outer scope has already finished by then, how does the inner function still remember the values of the outer variables?" This is exactly where the true power of closures come in. A closure keeps the references to the outer scope as long as the inner function hasn't executed yet. That means whether the code is synchronous or asynchronous, closures work the same way. Now, let's see a small asynchronous example to understand how closures work and why they are just as is in asynchronous scenarios. We define a function, function async example. Inside this function, we write var a is equal to 20. We have defined a variable. Next, we will use JavaScript's built-in set timeout function. For that, we write set timeout and pass a function. We all know that set timeout takes two parameters. One is the function to run, and the other is the time in milliseconds, meaning it will call that function after the specified delay. Now, suppose we put console.log a inside that function. Surprisingly, even though a isn't defined inside the timeout function, it can still access the a from async example's outer scope. This is possible because of closures. We did exactly the same, and then finally called the async example function. Now, let's check the output. After 3 seconds, it appears, and we see 20. This is also possible because of closures. The function inside set timeout doesn't have a defined within itself. Yet, it can access a from async example's scope. Now, what if we define the set timeout function outside async example just for demonstration, like this. Function my func. Inside my func, we write console.log a. Then, we pass my func into set timeout and also write console.dir my func. If we check the output of console.dir, we will see that inside my func, the closure contains the variable a is equal to 20. This is because a was part of async example's scope. So, my func can still access it. Just like before, this is possible thanks to closures. But here, there's a subtle difference. Earlier, we talked about a reference example, but this reference works a little differently. Suppose the variable a is equal to 20 was originally inside async example. Now, if we move A to the global scope and write var A is equal to 20, in simple terms, we have defined it outside. Now, the closure won't show it because it's part of the global scope. A will just exist in the global scope with a value of 20. We call the async example function, which starts the set timeout timer. Then, on the next line, after calling async example, we change the value of A. A is equal to 30. Now, think. If myFunc is called as the callback for set timeout and does console.log A, what value will it show? If you check the output, it will show 30. What does this mean? When async example is called, the set timeout starts and myFunc is ready as the callback. Inside myFunc, we have console.log A. At that moment, A was 20 in its parent scope, but since A is now global and its value has been changed from outside, when the callback executes, it shows 30. This demonstrates that closures actually hold a reference to the variable. If the variable is global, any external changes are also tracked. So, if we expand the global A in the console, we'll see A is equal to 30. I mentioned earlier that closures keep a reference to the value. So, when set timeout sends the callback to the main thread after 3 seconds, myFunc can still access that reference. Remember, myFunc comes back via set timeout from another place. It doesn't run directly on the main thread. This is part of asynchronous JavaScript. The function is called in the main thread after returning from the web API, but it still retains the reference to A. Since A has been changed globally, when myFunc prints it, it shows the new value 30. This point is very important. Practicing multiple examples will help you better understand how closures track outer variables in asynchronous situations. This is why you need to be careful when using global variables and var. This is also the reason var can sometimes cause conflicts and why the let keyword was introduced. For example, if we define var a globally and later change a somewhere in the program, any asynchronous functions referencing a will use the new value. That's why using var with setTimeout or other asynchronous functions can lead to such issues. An important point is that a closure doesn't keep the entire variable from its parent scope. It only keeps a reference to that variable. Now, let's see another practical example of closures. In typical JavaScript applications, we often make Ajax requests to fetch data from an API URL. We'll see how closures are used in this context. We know that for API requests like this, JavaScript's built-in fetch function can be used. Though, third-party libraries like Axios or jQuery Ajax can also accomplish the same task. We will use fetch for a practical example. First, we write a function function API function and pass the URL. We named it API function and gave it a parameter called URL. This function will send a request to that URL. So, what does fetch do with the URL? Basically, fetch returns a promise. We know that to get the output from a promise, we use then. So, we write {dot} then res and return something. After the response comes back, we use a callback function. Here, we log the response to the console. console.log and pass the res variable. This is how our API function is set up. Now, we will call the function. We We to pass a URL for the API call. A popular choice is JSON placeholder. So, we use it {slash} to-do {slash} one endpoint. We write API function and pass the full URL. Let's check the output. Notice that it appears after a short delay. This is asynchronous. To make this clearer, we write another line below. console.log I am here. Now, the question is which prints first? Without a doubt, I am here prints first and then the output from API function appears. This clearly demonstrates the flow of asynchronous operations. Since the response comes quickly, it might not have been obvious without this extra line. What does this mean? The output is coming, right? Here, the connection to closures is that we pass the URL parameter from outside when calling API function. This URL now exists inside the body of API function. Fetch takes it as a parameter and then the callback function inside then executes much later. By the time the call to API function has already finished, but the callback still remembers the variables from its scope. That's why even after the result arrives, we can still access URL. To see this, we can print it. console.log URL. Notice that the output is correct. This means that if there were more nested functions inside then, like another then inside a then, all the way down, the innermost function could still access the original URL. And this is possible only because of closures. To demonstrate, we can rewrite the callback function inside API function a little differently. function handle response This function simply does console.log URL. Now, we pass handle response into then. Then, we write console.dir handle response. In the output, we'll see that inside handle response, the closure contains URL. This is because it was part of API function's scope, so it can access it. Hopefully, this makes it clear. Finally, let's look at another example that often comes up in job interviews. This one is a bit more complex and tricky, and it shows how using closures inside loops can create unpredictable output. Suppose we write a simple for loop. For let I is equal to zero, I less than three, I plus plus. This loop will run three times, and inside it we define another function, function A. Inside this function, we simply do console.log I. From this, we see that the function A exists within the scope of the for loop, but in reality, it's also accessible in the global scope. Then, we call the function A. The expected output would be 0 1 2. First, the value of I prints zero, then one, then two, one after another. Now, looking at the output, we see 0 1 2. This is because we defined I using let. If we remove let and use var instead, what happens? Even with var, the output will be the same in this simple case, because var works outside the block scope. That is, writing for var I is equal to zero, or declaring var I separately behaves effectively the same. So, in this case, there's no problem. A closure isn't required because we are running the function in the global scope. Our I prints zero, then one, then two. Everything works correctly. Now, let's go back and use let for I again. Suppose we want to call the A function from outside the loop. Imagine we wrap the whole thing in a set timeout, and the first parameter, we pass the body of A as a callback, while the second parameter is 1,000 into I milliseconds. Using this 1,000 into I, we want zero to print after 1 second, one after 2 seconds, and 2 after 3 seconds. When we run this, the output comes exactly as expected. After 1 second, 0 prints. After 2 seconds, 1 prints. And after 3 seconds, 2 prints. But here's the important point. The for loop itself is synchronous, while the functions inside set timeout are asynchronous. That means the functions inside set timeout will execute one by one according to the timer. Only after the loop has finished. First, after 1 second, then after 2, and then after 3. We can verify this asynchronous behavior. Suppose at the end of the loop, we write console.log after for loop. Now, if we check the output, after for loop prints first. Then after 1 second, 0 prints. After 2 seconds, 1 prints. And finally, 2 prints. This clearly shows how asynchronous functions execute, right? No confusion there. The interesting part is if we use var I instead of let I, the behavior changes. All three outputs end up being three. We don't get 0, 1, 2 like before. That's exactly the tricky part of this question. This question often comes up in job interviews because it's a bit advanced. But if you understand closures and the difference between let and var scopes, it's not complicated at all. Let's analyze this by going back to the let. So, remove var I and write let I is equal to 0. Now, the expected output is 0, 1, 2. Let is block scope, meaning this I exists only inside the loop and has no effect outside. During the first iteration, I is 0 and the set timeout function is defined. This function will be called after the loop finishes. So, a closure is used to remember the value of I. The callback needs a closure to reference I correctly. Since let doesn't leak outside the loop, each iteration creates a new I. For example, when I becomes one, it's a completely separate I from the previous iteration. So, when this function runs the second time, it references this new I. But with var, the situation is different. Var is function scoped, so the same variable exists outside the loop. If we write var I and then use the loop, there is only one I. Changing I inside the loop modifies the same I. No new I is created. So, when the set timeout callbacks execute, they all reference that same I. From previous examples, we know set timeout callbacks run after the loop finishes. Now, after the loop ends, what's the value of I? Since we used var, I has become three. Check it with console.log I. In the console, you will see three prints first. That means when the callbacks execute, they all reference the same I, which is already three. So, every console.log in the callbacks print I is equal to three, which explains the output perfectly. To understand this even better, we can use console.dir like before. Let's see how. First, we'll stick with the let case. So, in the for loop, we write let I is equal to zero. We comment out the global var I since let is block scoped. Now, let's see how the closure works. The closure is created inside the set timeout callback function, and we want to inspect this callback function. For that, we define function my func and pass this my func inside set timeout. Then, to inspect it, we write console.dir my func. If we run this, the browser shows the same output. That means the dir of my function appears three times, but in Chrome's console, it only prints once. Chrome wraps similar objects together. So, even though the internal properties are different, it doesn't display them separately. To see each property individually, we take the next step. Below the DIR, we write console.log triple hyphen. This acts as a separator. Now, when the browser prints my funcs DIR, it also prints the separator, making it clear that each instance is separate. At the same time, outside the for loop, we add console.log after for loop. Now, if we check the output, the browser prints after for loop first, then 0 1 2. In my execution, when we defined it, the console logs show my func and the dashes. Notice when I is 0, the closure holds I is equal to 0. When I is equal to 1, the closure holds I is equal to 1. When I is equal to 2, the closure holds I is equal to 2. So, all three values exist as references until the end, which is why we get three separate outputs. But, what happens if we replace let I with var I? After printing after for loop, all three outputs show three. How? When I is 0, there's no closure because var moves this variable to the global scope. Unlike the previous example, no closure is needed here. Var exists in the global scope, and I changes within that same global variable. So, if we expand the first my func in the console and look for I in the global scope, you will see I is equal to three. Why? Because the for loop finishes first, and at the end, I becomes three. At the moment of after for loop, I is three. This means when the reference values are called, they still use the reference to that I. Even if I changes later in the program, since the calls happen afterward, the reference values get the updated I. That's why the first call shows three, the second call shows three, and the third call also shows three. If you expand them all, you will see I is equal to three everywhere. This happens because no closure is used here. It's referencing the original I in the global scope, which keeps updating. The difference in scope between let and var is why the output changes completely. Hopefully, this is clear now. To fix this var issue, now we can create an IIFE inside the for loop. This IIFE will take one parameter. In this case, the value of our loop variable I. All right. Let's write the code. Inside the IIFE, we are passing a parameter named I. Of course, you can name it anything you want. You already know that. But for now, we'll just keep it as I. Then, we call the IIFE. We'll pass the loop's I value into it. Nice, right? Let's see what the output looks like. This time, we get zero, one, and two correctly. So, why is it fixed now? Because I is now inside its own scope within the IIFE. Whenever we pass I to myFunc, a separate copy of that I is created as a parameter inside myFunc, and that copy is what gets used inside the function. Everything is clear now. If we expand the DIRS at the end, you will see the last one has I is equal to two in its closure. The second one has I is equal to one, and the first one has I is equal to zero. Perfectly clear, isn't it? So, if everything is clear, I would say if you practice all these examples repeatedly, your understanding of closures will become much stronger. Of course, there are more complex examples of closures, but the basics we have covered today are the most important. Once you understand them step by step, you will be able to create many use cases yourself, and debugging will no longer be difficult. Because now, with just a glimpse at console.dir or by playing with the code a bit, you can see how closures actually work. In summary, not understanding closures can make you get stuck in many parts of JavaScript, specially when working with asynchronous code. That's why I explained this topic in such detail today, went through so many examples, so that everything becomes clear in one go. Now, if you are asked in a job interview, what is a closure? You can answer simply, a closure is a mechanism where a function remembers variables outside its own scope and can access them whenever needed. In other words, values that aren't inside the function itself, but the function takes a reference from its parent or outer scope. This is what we call a closure. So, closure is equal to function plus remembered values. This is why closures are so important in job interviews. They show how deeply a programmer understands JavaScript. If I want to recruit a JavaScript developer, a good JavaScript developer, then I will definitely put a closure related question in the interview. A programmer who understands closures can clearly grasp JavaScript's internal behavior, memory handling, and asynchronous flow. JavaScript was originally created just for small interactive tasks in the browser. But now, you can build a large-scale applications, even back-end systems with it. The reason behind this is powerful concepts in JavaScript like closures, prototypes, and more. Many people say, "I know var, let, const. So, tell me about closures." But as you have seen, var, let, and const aren't that simple, either. This is where the understanding of closures begins. So, this was my detailed tutorial on closures for today. If watching it all at once feels overwhelming, you can break it into parts and watch it step by step. We have tried to explain the whole topic very simply, step by step. If there are any mistakes or if it took so long, we sincerely apologize. Still, I believe if you go through the entire tutorial, you won't have any confusion about closures. And from now on, you will never be intimidated by the word closure again. Thank you, everyone. You write JavaScript log a variable and boom, undefined or even a scary reference error. What just happened? Let's finally understand what's going on once and for all with a real story. Meet Sam. Every morning, Sam goes to a coffee shop. One day, before the waiter even arrives, Sam shouts, "I'll have my regular coffee." The waiter is confused. Um, "We haven't even given you the menu yet, sir." Turns out, they changed the menu yesterday. No more regular coffee. This is exactly what happens when you try to use a variable before declaring it. You're shouting at JavaScript, "Give me the value." But, JS says, "Hold on. I haven't seen that variable yet." Now, let's reveal the real twist. JavaScript does know that the variable exists. It just hasn't given it a value yet. Why? Because of something called hoisting. Hoisting means lifting or moving something above. JavaScript moves all declarations, not values, to the top of the scope. Let's first understand the difference between declaration and initialization. Declaration is when you tell JavaScript that a variable exists. Initialization is when you assign a value to it. Check the example on the screen. Here, we have declared a variable called coffee and assigned its value to latte. So, var coffee is the declaration and coffee equals latte is the initialization. When we run this code, we see undefined, not an error, because behind the scenes JavaScript rewrote the code like this. So, var is hoisted and initialized as undefined. Only the declaration part is lifted or hoisted to the top of the scope. This is called hoisting. It only affects declarations. Initializations stay where they are. That's why coffee equals latte. This remains on the same place or line. This is the first rule you have to remember. We saw the use case with var. What about let? Now we will see how let and const behave. See this example. Here, we tried to log the value of T before it was declared and initialized. But this time, we get a reference error. That means JavaScript has no reference to the T variable at that point. Surprising, right? It shows that let behaves differently from var. Let's take a closer look to understand what's really going on. Because variables declared with let and const are also hoisted. But they are kept in something called the temporal dead zone. What does that mean? Well, temporal dead zone is the time between when the scope starts and when the variable is declared. The scope starts here and the variable is declared here. During this time, the T variable is in the temporal dead zone. Which means in this period, the variable exists, but you're not allowed to touch it. Think of it like this. You're at an event. There's a gift with your name on it, but the host says, "Don't touch it until I officially hand it to you." If you touch it too early, boom, reference error. Let's summarize this in one simple table. Hoisting happens for all, but only var is accessible before the line where it's declared, but not let and const. This is the second rule you need to remember. So far, we have seen hoisting in global scope. Now, let's check what happens inside block scope. Check the code example shown on the screen. Here, the rule is also same. Block scoped variables like let and const hoist within that block only. Will not go out of the block and are still in temporal dead zone. And var is function scoped, not block scoped. So, it still works badly even inside a block. Simple. Now, see what happens for functions. Function declarations are fully hoisted including the body. So, you can call greet before it's declared and it works fine. But, this doesn't work because say bye is a const. So, it follows the temporal dead zone rule, not the function rule. Same rule applies to the arrow functions. So, function declarations and function expression works differently. Here are my tips or best practices to avoid hoisting confusion. Always declare variables at the top of their scope. Use let and const. Avoid var. Don't rely on hoisting. It's a behavior, not a feature. Structure your functions to read top down. Use linters like ESLint. They can catch temporal dead zone bugs. Here's the mindset tip I promised. Think of hoisting as JavaScript's preprocessing stage. It scans your code before running it, pulling up declarations, but not their values. So, next time your code acts weird, don't panic. Check for hoisting. Check for temporal dead zone. And write your code like you respect the timeline. Thanks for watching this complete guide to JavaScript hoisting. If this helped you, like the video, write a comment, "Hoisting makes sense now." Keep learning and keep building. See you next time. Hi everyone. Hope you all are doing well. Today's topic is JavaScript's execution context. I wanted to make sure we first cover the fundamental concepts of JavaScript before diving deep into execution context. Once you finish watching today's video, you will realize that many of the topics we discussed earlier, like scope, hoisting, and closures, will start making much more sense and feel newly discovered from a deeper perspective. We already have a preliminary idea about these topics. So, today, through the lens of context, we're going to rediscover all those concepts by understanding execution context in a completely new way. And by the end, you will finally understand how everything connects together. How two hands make four. So, without wasting any more time, let's get started. Before diving into JavaScript context, let's take a step back and see how JavaScript actually runs, whether in a browser or in a terminal. You might be wondering why I'm mentioning the terminal. Well, that's because JavaScript now also runs outside the browser through Node.js. So, let's first explore how that happens behind the scenes. So, the code you write in JavaScript, when it reaches the browser, or in the case of Node.js, the server, it's ultimately executed by the computer. But, here's the thing. Computers don't understand JavaScript. They only understand binary or machine language. So, somehow, the browser has to translate our JavaScript code into machine language so that the computer can actually run it. That means there must be some kind of mechanism inside the browser or in the Node.js runtime that performs this translation from our programming language to machine language, right? Exactly. That mechanism inside the browser is called the JavaScript engine. In Google Chrome, this engine is known as the V8 engine. Other browsers use their own engines. Firefox uses SpiderMonkey, Internet Explorer uses Chakra, and Safari uses JavaScript Core. No matter which engine it is, every browser has one, and they all perform the same core task, converting our JavaScript code into machine language. Different browsers may implement this process in their own way, but all of them follow the same standard, known as the ECMAScript specification. ECMAScript is basically the official standard that defines how JavaScript should work, like ES5, ES6, and so on. Since each browser's engine implements the standard slightly differently, sometimes the same piece of code may produce slightly different outputs in different browsers. Anyway, once our code reaches the browser, it's handed over to the browser's JavaScript engine. The engine then converts that code into machine language, which the computer can finally understand. And that's how we get to see the output displayed on our screen. So, that's the overall top-level view of how JavaScript code runs. Now, as programmers, we don't really need to understand what happens at the hardware level, because our focus isn't building hardware. We are application developers. What truly matters for us as JavaScript developers is understanding what happens inside the engine when it runs our code. In other words, we need a clear picture of how the JavaScript engine actually compiles and executes our code. That's the foundation of everything. So, let's start right there. But before we do, there's one important thing I should mention. I have been talking mostly about browsers, so you might be wondering what happens in the case of Node.js? Well, Node.js actually uses the same V8 engine that powers Google Chrome. Ryan Dahl, the creator of Node.js, is a truly brilliant programmer. What he did was take Chrome's V8 engine, which was already built, tested, and extremely fast, and use it outside the browser. V8 is one of the most powerful and highly optimized compilers in the existence. It converts JavaScript into machine language with incredible speed and efficiency. That's why Google Chrome performs so well because of the V8 engine. Ryan Dahl realized that V8 was already the best engine out there. So, he thought if he could extract it and use it outside the browser, he wouldn't need to build a new engine from scratch. The compilation part would already be handled by V8. Since V8 was written in C++, Ryan integrated it directly into his own C++ program. In other words, his C++ code and the V8 engine were combined, and that's how the Node.js runtime was created. Now, let's take a look at what actually happens inside a JavaScript engine when it compiles JavaScript code. To understand that, we need to start with a bit of history. When JavaScript was first introduced, it was an interpreted language, meaning there was no compilation process. There wasn't a compiler to convert JavaScript into efficient machine-level code. And here, we come across two important terms: interpretation and compilation. At first, interpretation and compilation might sound like the same thing because both convert our code into machine code, right? That's true in essence, but the way they work is completely different. Let's take a closer look at the difference between interpretation and compilation. Here, we have some JavaScript code written by us. This code is readable to humans, but the computer doesn't understand it. So, what does a computer understand? It only understands binary, zeros and ones. That's machine language, and while the computer understands it perfectly, we humans can't read it. So, our job is to take the human readable JavaScript code and convert it into machine code so the computer can understand what we are asking it to do. There are three main ways to perform this conversion. Interpretation, compilation, and the number three is a hybrid approach, a mixture of both. Back in the '90s, JavaScript used to be a purely interpreted language. But in recent times, modern JavaScript engines use a combination of both interpretation and compilation to translate code into machine language. The language itself hasn't changed. It's still that JavaScript, but the way it's implemented has evolved dramatically. Now, let's see how an interpreter actually works. How JavaScript code gets converted into machine code through interpretation. Inside the interpreter, there's already a set of equivalent machine instructions for every command or expression we write. For example, look at this piece of JavaScript code. It contains several lines of instructions. The interpreter already knows the corresponding machine level operations for each of these lines. Suppose we have a variable where two numbers are being added. That's an arithmetic operation. Then, in the next line, there's a console log statement that prints something on the screen. And there could be many more such instructions in our code. For every one of these, the interpreter has a predefined binary or machine code equivalent, which tells the computer exactly how to execute that particular command. So, we can visualize it like this. When we run a program, the interpreter starts executing the instructions line by line. First, it looks at the first line where two numbers are being added. The interpreter already knows how to handle that operation. So, it takes that instruction, translates it into binary, and executes it. Once the first line is done, it moves to the next line. Let's say that's a console log statement. The interpreter knows how to execute that, too. So, it runs it. Then it moves to the next line and the next and so on. But, there's a problem. This process is quite slow because the interpreter has to execute one line at a time, constantly going back and forth between reading the script and executing it. This repeated process makes pure interpretation significantly slower. That's why JavaScript was very slow back in the '90s. However, this process did have one big advantage. You might wonder, if it was so slow, why was it designed this way? The reason is that JavaScript was very easy to debug at that time. Here's why. The interpreter executes one instruction at a time. So, if it encounters an error while running any particular line, it can immediately stop and notify the programmer exactly where the problem occurred. This made debugging much easier because developers could instantly identify the issue, fix it, and continue running the code. So, the advantage of interpretation was that it was easy to debug. But, the downside was that it was slow. To overcome that slowness, the concept of compilation was introduced. Let's now see how compilation works. A compiler works differently. Instead of executing code line by line, it takes the entire program at once, translates the whole thing into machine code, and then runs it line by line from that compiled version. This makes the process much faster compared to interpretation. However, there's a problem here, too. Suppose there's an error in one of the lines of code. The compiler won't stop there. It will continue compiling the rest of the code. That means if your program contains any malicious or faulty logic, for example, an infinite loop or a piece of code that causes a memory leak, it might still get executed and potentially crash your system. You won't have the chance to stop it midway like in the interpreter's case. So, that's the major drawback of compiled languages. They can crash the system and are harder to debug. Why? Because the entire code gets compiled first, so we don't immediately know which exact line contains the error or bug. To find that out, we have to run the program first, and only then can we detect where the problem is. In short, with interpretation, we had the advantage of easy debugging but slower execution. On the other hand, compilation gives us speed but makes debugging difficult and increases the risk of system crashes. So, imagine if there were a way to combine both to get the best of both worlds, fast execution and easy debugging. That's exactly what Google Chrome did in 2008 when they introduced the V8 engine. They combined these two approaches into a single system and called it a just-in-time compiler or JIT compiler. This JIT compiler merges the easy debugging nature of interpretation with the high-speed execution of compilation, creating a highly optimized and balanced process. This approach might not be as fast as fully compiled language, but it's definitely fast enough that users can't tell the difference in real-world performance. Now, let's see how the JIT compiler actually works. In JIT, the just-in-time part behaves like an interpreter. It reads the code while the compiler part translate it into machine code. Here's what happens. When we write our JavaScript code, the JIT compiler doesn't immediately compile the entire thing. Instead, it keeps the code as it is. When a particular function or instruction is invoked, say for example the instruction one function, the JIT compiler instantly compiles that specific function into machine code, executes it, and returns the result. Now, suppose we call another function, instruction two, and an error occurs inside it. The JIT compiler would immediately detect it and say, "I have just-in-time compiled this function, and there's an error here. Stop right there. This allows the programmer to instantly see where the issue is and fix it, maintaining both speed and debugging flexibility. That means we can still debug easily, so it's both easy to debug and fast enough to run efficiently. In other words, it's truly the best of both worlds. That's the core feature of the JIT compiler, and this is exactly how modern JavaScript engines, including Node.js, work today. They compile JavaScript through just-in-time compilation. I hope this part is clear to you. So, with that understanding, let's move on to today's main topic, JavaScript's execution context. Many people start learning directly from execution context, but I personally believe it's important to first understand how the compilation process actually works. Now that you have that foundation, when we dive into execution context, your brain will be able to connect both parts and visualize the complete picture of how JavaScript truly operates. So, let's move on to our main topic, JavaScript execution context. In my opinion, execution context is the most important concept in JavaScript. The reason is simple. If you truly understand how it works, then advanced topics like hoisting, scope, scope chain, and closures will become incredibly easy to grasp. Let's start by thinking about how we usually write code. One common strategy is to break our code into smaller parts. These separate pieces of code can have different names, like functions, modules, or packages. But no matter what you call them, their purpose is the same, to break a complex program into smaller, more manageable chunks. This division reduces complexity and makes our code easier to read, maintain, and debug. On the screen, we can see an example. On the left side, there's a block of code, and on the right side, the same code is divided into smaller functions. At first glance, the code on the left might look simpler, but if you look closely, it's not really practical. In this example, I have represented each action with a simple console.log statement, just for easy understanding. But, in a real-world application, things aren't that simple, right? Instead of just console logs, there would be complex logic, multiple algorithms, API calls, and data processing happening behind the scenes. Now, imagine if we wrote all those complex operations like taking orders, processing them, and completing them all inside a single file or function. That would make our code neither easy to read nor easy to maintain. It would quickly become messy and difficult to debug. But, if I break each task into smaller functions like the ones on the right side, it becomes much easier to maintain, and the overall complexity of the code decreases significantly. In the same way, JavaScript also break down our code into smaller parts before interpreting it. This helps reduce the complexity of execution. Each of these smaller units is what we call an execution context. That's the simplest definition you can think of. An execution context is basically a small, isolated environment where a specific piece of code is interpreted and converted into machine language. So, to make its job easier, the JavaScript engine divides our code into smaller parts and executes them one by one. Each of those parts is an execution context. In today's session, we won't be writing any code. Instead, I will walk you through how execution contexts are actually created inside the JavaScript engine, line by line, so you can clearly visualize what happens behind the scenes when your code runs. Let's imagine the left side of the screen is our code editor, where we'll be writing the code, and the right side will show how the execution context is created. Now, at this point, the left side, our code editor, is completely empty. Even when there's no code written yet, JavaScript still creates something called a global execution context right at the very beginning. So, what exactly is this global execution context? Think of it as a simple object, or you can visualize it as a box, a container that holds everything your program needs to start running. At the very start, before any code is executed, this global execution context is created. Inside it, the first thing we have is the window object, which I'm sure you are already familiar with. Next, we have the this keyword, which initially points to the window object. Then comes the variable object, and finally, another structure called the scope chain. There's one more important thing to understand. The global execution context actually goes through two distinct phases. When the program runs for the very first time, it enters what's called the loading phase, or creation phase. During this phase, your code doesn't execute yet. To summarize, when the global execution context is first created, it contains four main components: window, this, variable object, and scope chain. And before any line of code runs, JavaScript first goes through the loading phase. Now, let's assume we have written some code. It's just a short seven-line script where we have a variable named topic and a function called get topic that simply returns that topic. As we have just discussed, when the global execution context is created, it already includes window, this, variable object, and scope chain. Along with that, during the loading phase, any functions and variables declared in our code, like topic and get topic, also get added to the global execution context. But, here's an important point. During the loading phase, these variables and functions are only recognized, not yet fully executed. That means their values are temporarily set to undefined until the actual execution begins. As I mentioned earlier, there's something inside the global execution context called the variable object. During the loading phase, JavaScript allocates a specific space in memory for every variable declared in the code. So, for example, a memory slot is created for our variable topic, and JavaScript assigns it the value undefined at this stage. Similarly, the get topic function is also stored that same variable object. But, instead of assigning it as undefined value, JavaScript stores a reference to the function, meaning the function's entire structure or body is saved somewhere in memory. So, now through the global execution context, JavaScript already knows that there's a function called get topic defined in the program. It hasn't executed it yet, but it's aware that such a function exists and might need to be called later during execution. Once the creation phase is complete, JavaScript moves on to the second phase, the execution phase. In this phase, the program starts running from the very beginning, line by line. So, when execution begins, it looks at the first line, var topic is equal to JavaScript execution context. Now, JavaScript already knows the variable name topic because during the loading phase, it had already stored it in memory with the value undefined. When the execution phase starts, JavaScript simply replaces that undefined with the actual value we have assigned. This is the first time the code truly gets executed. During the creation phase, JavaScript also stored the get topic function in its memory. It knows that somewhere in this program, there's a function called get topic. So, when it reaches line seven, where the function is called, it retrieves that function reference from memory and executes it. Now, imagine we write console.log topic at the very top of our code, meaning we are trying to use the variable before it's even declared. In earlier tutorials on JavaScript hoisting, I explained this concept, but not in this much detail. Not in the context of the execution context. Back then, I said that during hoisting, JavaScript conceptually moves all the variable declarations to the top of the scope. Though, only the declarations, not the actual values. But now, through the lens of the execution context, you will understand what really happens behind the scenes. As we saw earlier, during the loading phase, JavaScript already stored the variable topic in memory and assigned it the default value undefined. That's why when we try to print topic in the very first line, before it's declaration, the console will show undefined, which is the value set during the loading phase. Then, when the program enters the execution phase and reaches the line var topic is equal to JavaScript execution context, if we place our console log after that line, we will see the correct value, right? That's because during execution, when JavaScript encounters this line, it updates the value of the topic variable in the execution context from undefined to JavaScript execution context. Since the value is now updated, any console log that runs after this point will display the correct value. So, if you have watched my earlier tutorial on hoisting and now you are watching this video, you will finally understand why this hoisting behavior happens inside the JavaScript engine. In that tutorial, I explained hoisting as variables being moved to the top, which was just a simplified way to help you visualize it. But now, through execution context, you can see what actually happens behind the scenes. I hope this part is crystal clear now. To summarize, the global execution context is basically a JavaScript object. At the very beginning, it contains the window object if you are in a browser environment or the global object if it's Node.js. There's also a this object that points to the same window or global. Then, all the variables declared in our code are stored inside something called the variable object. And any functions we define are also stored there, but only as references. Meaning, JavaScript just keeps a pointer to their full body in memory. Then, when the program enters the execution phase and reaches the line var topic is equal to JavaScript execution context, if we place our console.log after that line, we will see the correct value, right? It also keeps something called a scope chain. As we know, JavaScript uses lexical scoping, meaning each function knows exactly where it was written and can access variables from its outer scopes. So, JavaScript maintains all these scopes together inside the scope chain. That was the visual representation of the global execution context. Now, let's go a bit deeper. By this point, you have already understood how the global execution context works. The good news is that after this, you only need to understand one more type of execution context, the function execution context. And the interesting thing is, it's almost identical to the global execution context. The only difference is that it's created every time a function is called. Let's quickly recall what happened inside the global execution context. It created a global object, created a this object, allocated memory for variables and functions, and initially assigned all variables the value undefined, right? Now, if you think carefully, which of these four steps do you think won't be necessary when a function gets executed? Exactly. The global object shouldn't be created again inside a function execution context because the entire program already has one global object that was created earlier and all functions can access it when needed. So, instead of creating a new global object, something new happens inside the function execution context. It creates an arguments object. This object holds all the parameters passed to that function. That means whenever we define a function in JavaScript and it has parameters, JavaScript automatically creates an object called arguments inside that function's execution context to store those values. If we write console.log arguments inside the function body, you will see all the past parameters neatly stored as key-value pairs within that arguments object. So, the only structural difference between the function execution context and the global execution context is this: the global one contains a global object while the function one contains an arguments object. Everything else remains exactly the same. Simply put, think of the global execution context as an entire world. Whenever you call a function, a new world is created inside that global world following the same structure and behavior. I hope that makes the idea clear. When a function is called, JavaScript creates a completely separate execution context for it. Inside that context, it builds an arguments object to hold all the parameters, creates a this object as usual, and allocates memory for all the variables and inner functions defined inside that function. That means hoisting also applies inside functions. During the creation phase, JavaScript will assign undefined to all variables within that function and that's exactly why hoisting happens there, too. And this is how the function execution context works. Now, remember how we said that in JavaScript one function can contain another function? That means there can be multiple execution context. The global execution context is always there and alongside it multiple function execution context can be created as different functions are called. So obviously JavaScript needs a way to keep track of all these contexts somewhere. And since JavaScript is a single threaded language, meaning everything runs on one main thread, that same thread is also responsible for managing all these execution contexts, creating them, stacking them and clearing them one by one. JavaScript actually stores these contexts one on top of another, just like a stack. And this is stacking structure is called the execution stack or more commonly the call stack. At the very beginning JavaScript places the global execution context at the bottom of the stack. Then whenever a function is called a new function execution context is created and pushed on top of it. If that function calls another function inside it, another context is stacked above that and so on. In this way JavaScript keeps adding and removing execution contexts in a stacked order as the program runs. So now we have a complete picture of what an execution context really is. It's made up of two types, the global execution context, the function execution context. Now let's move on to something a bit more advanced. So far we have only seen how the execution context works in the global scope or when a function is called directly from the global scope. But what if we have multiple functions or if one function contains another function, in other words, nested functions? How does the execution context behave then? Because every time JavaScript creates an execution context, it needs to keep track of them somewhere in memory. There has to be some logic to determine which context should run first and which one should execute next. To manage all of that, JavaScript needs to maintain a specific data structure, right? And that data structure is called the execution stack. It's actually based on a stack data structure. Think of it like stacking books on a shelf. You place one book on top of another. Similarly, JavaScript stacks each execution context on top of the previous one inside the execution stack. Now, a stack has one special property. The last item that goes in is always the first one to come out. In short, it follows the LIFO rule. That means last in, first out. All right. Let's connect this idea with our code. We know that whenever we run a JavaScript program, the global execution context is created first. Whether our file contains code or not, this global context will always be created automatically. And once it's created, JavaScript places this global execution context at the very bottom of the execution stack. That's where everything begins. Next, let's look at our code. Inside it, we have three things. A variable declared with var A, a function called one with its definition, and finally a call or invocation of that function. These three, the variable A, the function one, and its invocation, all exist in the global scope or the root level of the program. Inside the body of the one function, there's another nested function called two. And inside two, there's yet another function called three. These nested functions are not part of the global scope. They belong to their respective inner scopes. So, during the creation phase, JavaScript stores the variable A with the initial value undefined and registers the body of the function one with the variable object. Since there's nothing else in the global scope, JavaScript then moves on to the execution phase. In this phase, execution starts line by line. First, it updates the value of A from undefined to one. Then it moves to the next line, where it finds the function definition of one. Since that function's reference is already stored in the variable object, JavaScript skips over the function body for now and continues to the next line, the point where one is actually invoked. As soon as the one function is invoked, JavaScript creates a brand new function execution context inside the execution stack, placed right above the global execution context. When this new context for one is created, it first enters its loading phase. Since the one function doesn't take any arguments, the arguments object inside this execution context will remain empty. Then comes the this reference, which points to the global object, in this case, the window object. After that, we have the scope chain and the variable object. Inside the variable object, there's only the two function. Why? Because inside the body of one, there are no local variables declared, only a console.log statement that tries to print A. But since the variable A is not defined inside one, it doesn't appear in this context variable object. Instead, JavaScript will later look for it in the outer scope using the scope chain. However, the one function body does contain another function declaration, the two function. So, during the loading phase, JavaScript stores the two function inside the variable object, just like it stored the function definitions earlier in the global execution context. It follows the exact same process, only this time it's happening inside the function's own scope. All right. Now, let's check if there's anything else inside the body of one function that needs to be added to its variable object. The answer is no. There's nothing else. So, next, JavaScript moves to the execution phase, and just like in the global execution context, it starts executing line by line. The first line inside the one function is console.log A. At this point, JavaScript checks whether the variable A exists inside the variable object of the current execution context. It looks and finds nothing. Since the variable A isn't declared inside the one function, JavaScript moves to the next step. It follows the scope chain to look into its parent scope. Now, what's inside the parent scope? Yes, the variable A is there, defined in the global execution context. So, JavaScript retrieves that value and prints it in the console. It's important to clearly understand one thing here. The scope chain is essentially a lexical environment. This means every scope is connected to its parent scope in a linked structure. The term chain is used because each scope holds a reference to its parent or ancestor scopes, forming an actual chain-like connection. That's why when JavaScript couldn't find the variable A in the one function's own variable object, it followed that chain upward to its parent or ancestor scopes and successfully found A in the global scope. Hopefully, that part is clear. Now, let's go back to the code flow again. After printing the value of A, the program moves to the next line. There it finds the body of the two function. But at this stage, JavaScript doesn't need to do anything with it because during the loading phase, the reference to two has already been stored in memory. So, it skips over the function body and moves to the next line where it sees that two is being invoked. Since the two function is now being called, JavaScript creates a brand new function execution context for it and places it on top of the execution stack. Just like before, when this new two execution context is created, it first goes through its loading phase. During this phase, it populates its scope chain, meaning it links itself with the variable objects of its parents or ancestor scopes. Inside the body of two, there's a variable named B. So, in this new context variable object, JavaScript stores B with the initial value undefined. Then it finds another function definition, three. So, just like before, JavaScript stores the reference to the three function inside the variable object of two. Then, in the next line, the program moves to where the function three is called. Since it's still in the loading phase, there's nothing to execute yet. That part is done. So, now it enters the execution phase and starts running each line one by one. Inside function three, what's the first line? It's a console log of the variable B. But that variable hasn't been initialized yet. In this context, its value is still undefined. So, when the program runs that line, it prints undefined. Now, you can clearly see why during hoisting, variables often print undefined. Once you understand how the execution context work, many other tricky behaviors of JavaScript starts to make sense. So, pay close attention and try to grasp these terms deeply. All right. After that, in the next line, the variable's value is being assigned. So, the value of B changes from undefined to two. Then, in the following line, we see the definition of the three function. The execution context doesn't need to do anything here. So, it simply skips over this line. The next line shows that the three function is being invoked. And not just that, it's being called with an argument four. Since the three function has been invoked, a brand new execution context is created inside the execution stack. Now, pay close attention here. During the loading phase of the three execution context, we can already see that there's a value inside the arguments. At index zero, it's four. That's because when the function was called, the value four was passed in. And here's something interesting. Look at how we named the parameter inside the three function. It's called D, right? So, during the loading phase itself, a variable named D appears inside the context and it's already assigned the value four. Then it moves to the next line. There's a console.log statement. Nothing special happens there yet. After that, we have let C is equal to three and this is where things get a bit different. You see, although var, let, and const are all used to declare variables, their behaviors are not the same. For var, during the loading phase, JavaScript automatically allocates memory and sets its value to undefined. But for let and const, JavaScript still allocates memory during the loading phase. The difference is they remain inside what's called the temporal dead zone or TDZ until the actual line of code where they are declared is reached. That means even though they exist inside the execution context, they can't be accessed through the variable object yet. Now, you might wonder what exactly is the temporal dead zone? It's the period between a variable being created in memory and being initialized with a value. During this time, the variable technically exists, but since no value has been assigned, JavaScript keeps it temporarily inaccessible. If you try to access that variable during the TDZ, you will immediately get a reference error. In simple terms, the program knows that the variable exists, but it's not ready to be used yet. And until the code execution reaches that specific declaration line, you won't be able to access it from the variable object either. You can think of it like an iPhone lock screen. The phone is there, but until you enter the PIN and unlock it, you can't do anything with it. Now, many people wonder, "Doesn't the same thing happen with var? Why does JavaScript assign undefined to variables declared with var, but keeps let and const inside the TDZ instead of doing the same?" Excellent question. The main reason is that var comes from the older versions of JavaScript, specially ES5 and earlier, where there was no concept of safety checks or the temporal dead zone. Back then, JavaScript didn't want the program to crash if a variable was accessed before initialization. So, to avoid breaking the program, it would automatically assign undefined as a fallback value. Just try to be a bit more smart. But, let and const were introduced in ES6, where the goal was to keep the language safer and more predictable. If JavaScript had assigned undefined to them as well, it would have created the illusion that was properly initialized even though it wasn't. So, JavaScript intentionally blocks access to those variables during that time to signal to developers. The variable exists, but it's not ready to be used yet. Hope that clears things up. Now, let's get back to our flow. At this point, inside the three function, there's nothing else left to load. So, it moves to the execution phase. In the first line, we have a console.log printing C plus D. We already know that the value of D is four, but the variable C is still inside the temporal dead zone. Meaning, it can't be accessed yet. So, this line will throw a reference error because the program can't reference C from memory at that point. However, if we had written the code the other way around, first declaring let C is equal to three and then logging it in the next line, the behavior would have been completely different. In that case, during the creation phase, C would still start in the dead zone, but by the time the execution reached that line, its value would already be assigned. Then, when the console.log ran, both C and D would be accessible and their values would print correctly. Clear now? So, you can see how JavaScript actually works behind the scenes. We have also learned how hoisting truly operates at a machine level through the concept of the execution context. All right, now let's move forward. Inside the three function, once the console.log runs successfully, it will print seven. After that, is there anything else left in the function? No, there isn't. Since there are no more lines to execute, the work of the three function is complete. And whenever a function finishes its job, it immediately gets popped out from the execution stack. That means, since the three function has finished executing, it will be removed from the stack following the LIFO. That means, last in, first out rule. Now, who's at the top of the stack? The two function. If we check the body of two, do we see any remaining lines to execute? No, it's done, too. So, two will also exit from the execution stack. Finally, since there's nothing left to run in one, that function will also pop out, leaving the execution stack completely empty. Since there are no more functions left to execute, the global execution context will also exit from the execution stack. However, if there had been more code to run in the global scope, for example, another function called right after one, then the global execution context wouldn't have been removed yet. Instead, it would have continued executing the next function just like before. I hope by now you have no doubts left about execution context. While learning about it, we also discovered how hoisting actually works, didn't we? Now, let's move on to the another important concept, scope. We have already discussed scope before, but this time we'll understand how scope works in relation to the execution context. Take a look at this simple example. Here, we have a function called hello. Inside it, there's a variable named message declared with var, which holds a certain value. Outside the function, that is, in the global scope, we are calling or invoking the hello function, and in the next line, we are trying to print the message variable using console.log. It's a very simple setup. So, based on everything we have learned so far, what will happen if we run this program? First, the global execution context will go through its creation phase. During this phase, it will find a function named hello and store its reference inside the variable object. Once that's done, there's nothing else left for the creation phase, right? Perfect. Now, the execution phase begins. The first line is the definition of the hello function. Since we're in the execution phase, there's nothing to execute here. The function's reference is already stored in memory. So, the program moves on to the next statement, where the hello function is invoked. As soon as that happens, a new execution for the hello function is created. Inside the creation phase of the hello function's execution context, the variable message is placed inside the variable object with the value undefined. Once the creation phase is complete, the program moves to the execution phase, where the variable message gets its actual assigned value instead of undefined. So, in the execution phase, the value of the message variable becomes hello world. After that, does the hello function have anything else to do? No, it doesn't. So, the hello function's execution context will now be popped off or destroyed. That means the program returns to the global execution context. Now, what happens next? After the hello function is invoked, the next line is console.log message. So, the program will now try to print message. But wait, is the message variable declared inside the global execution context variable object? No, it isn't. That variable was created inside the function execution context. And when that function finished executing, its execution context, along with its scope chain and variable object, was completely removed from memory. That's why when the console tries to access message, JavaScript throws a reference error. Can you relate this now? Those of you who have seen my video on a scope might remember the example I gave. A child can always access or inherit things from its parent, but a parent can never access what belongs to the child. Back then, it was just an example, but now you can actually visualize how the program manages all of this behind the scenes. That means, while understanding execution context, we have rediscovered the concept of scope in a much clearer and more practical way. Now, let's move on to another very important topic, closure. Just like we visualized execution context earlier, we will now see how a closure is created and how it works step by step. First, the program creates the global execution context. We are now in the creation phase. During this phase, the program scans through the code and finds a variable named sum and a function named do sum. So, memory is allocated for sum and its initial value is set to undefined. The entire function definition of do sum is stored in memory as a reference. Once the creation phase is complete, the execution phase begins. In the execution phase, the first line sets sum is equal to zero. Next, the program skips over the do sum function since it's only a definition. Then it moves to the line var temp is equal to do sum in the bracket two. Here, the function is being called, so a brand new function execution context is created for do sum. Now, the creation phase of that function execution context begins. The parameter A receives the value two. So, in memory, we have A is equal to two. Inside the function, there's an anonymous function being returned. That function's reference also gets stored in memory. Once the creation phase is done, the execution phase starts. During this execution phase, the do sum function doesn't perform any calculation. Instead, it simply returns that anonymous function. When that happens, the do sum execution context is popped off the stack, but something very important also occurs at that exact moment. A closure is created. Why does that happen? Because when the anonymous function is returned, it still remembers the data from its outer scope. In this case, A is equal to 2. Even though the do some function itself gets destroyed, its lexical environment doesn't disappear completely. JavaScript realizes that this inner function might be called again later, and when that happens, it will need access to those outer variables. So, it preserves that environment in a separate structure called the closure scope. Now, you can understand JavaScript is really smart. Next, the code moves to the line sum is equal to sum plus temp 8. Here, the function is called again, meaning temp 8 is invoked. This creates a new execution context for the anonymous function. During its creation phase, the parameter B is assigned the value 8. When the execution phase begins, the function executes return A plus B. Now, since A doesn't exist in the current scope, JavaScript follows the scope chain and looks into the outer scope, which is the closure scope. There it finds A is equal to 2, performs the calculation 2 plus 8 is equal to 10, and returns the result 10. At that point, the temp function's execution context completes its job and gets popped off from the stack. So, in the line sum is equal to sum plus temp 8, the value of sum was initially zero. That means it becomes sum is equal to zero plus 10. Now, where did that 10 come from? It came from the temp function's execution context. Now, you can clearly see how a closure actually works. A closure is basically a mechanism that keeps a function connected to its outer environment, even after the outer function has been destroyed. This is one of JavaScript's most magical, yet completely logical behaviors, and it all originates from the concept of the execution context itself. So, everything you have seen so far, this is what JavaScript's execution context truly is. By understanding just this one topic, we have been able to uncover how JavaScript actually works behind the scenes. We discovered how scopes are formed and how confusing topics like closure and hoisting are really operated logically within the language. All this time, we used to just repeat what we heard, that hoisting means variable are lifted to the stop and closure means a function stays connected to its outer environment. But after watching this video, you should now have a clear inside-out understanding of how and why JavaScript behaves this way. From today, you may not understand your wife, but you will definitely understand JavaScript. I have given my best effort to explain this topic as clearly as possible. If this video helped you understand these concepts better, please don't forget to like and comment. It truly encourages me to keep making more videos like this. And if you have any valuable feedback or specific topics you'd like me to cover next, feel free to let me know. Your small words of motivation inspire me to keep creating and sharing more. Thank you so much for everything and thank you so much for watching my videos. I will see you again in another one. From my personal experience, when I started working with JavaScript, it was back in 2008. At that time, JavaScript was considered very trivial. People used to say it was only for a few interactive web features. But even in that era, JavaScript already had the prototype concept. Still, I personally didn't work deeply with prototypes because I never felt the need to. But over time, JavaScript became one of the most popular programming languages in the world. If someone wants to learn JavaScript, they can't skip prototypes because the foundation of JavaScript is prototypes. This is what makes JavaScript different from many other programming languages. Since I'm talking about JavaScript, prototypes naturally have to be part of the discussion. So, that's why today's tutorial. I hope after watching this tutorial, you won't have any confusion about prototypes. Let's get started. In JavaScript, you can't go far without objects. Whatever you do, almost everything is based on objects. We know JavaScript is a functional programming language, and here functions themselves are objects. That means in JavaScript, you can't even imagine anything without objects. And JavaScript's prototype concept is connected to these objects. So, I don't want to start with a theoretical definition of prototypes. I will show you step-by-step how objects are created or instantiated. Then, gradually through that process, we'll discover prototypes on our own. If you understand this through examples, it becomes easier to understand, easier to remember, and the concept becomes clearer. Let's start. What's the most common way to create a new object in JavaScript? The most common way is this literal format. Let person is equal to a blank object. And just by adding curly braces, the object gets defined, right? So, person is a blank object. Now, if we want to add properties to the person object, how do we do that? Usually, we do it with dot notation. So, we write person.name is equal to Messi. So, now we gave the person a name, Messi. Next, let's add age. Person.age is equal to 38. Now, if we want to add any method or function to the person object, we can do that with the same rule. In JavaScript, functions behave like variables, and functions themselves are objects. So, they can be stored as object properties. So, let's say we want to add a function inside the person object. For example, humans eat. So, we'll create a function called eat. Person.eat is equal to function. Then, we'll print something inside the function. So, I will write here console.log person is eating. So, now we created an object that has regular properties, and we also added a function. Now, let's create another function. What comes after eating? Sleeping. So, let's create another function called sleep. Person.sleep is equal to function. And inside this function, I will also write console.log person is sleeping. So, we created a person object and added different properties and functions to it. But, in real-life applications, we may need to create many such person objects, right? In object-oriented programming, in large applications, we work with many objects. Let's say, instead of person here, we need to work with user objects and create many users. So, what can we do in that kind of situation? We can simplify this a bit instead of doing person.eat, person.sleep separately every time. But, before that, let's cut the whole code we wrote. Now, I will define a function. Function person name, age. I named the function person and passed name and age as parameters. Now, inside this function body, I will paste the code we cut earlier. So, here we had person.name is equal to Messi. Instead of hardcoding Messi, since we took name as a parameter, I'll write person.name is equal to name. That means, instead of a hardcoded name, now we are dynamically taking the name from the parameter. And similarly, for age, I'll write person.age is equal to age. That means, instead of the hardcoded 38, I'm taking age from the parameter. This part should be very clear to everyone. And finally, since I made this a function, I want an object back. So, at the end, I will return the object from inside the person function. Return person. All right. So, this way we did the same thing as before. We know functions themselves are objects. To see that, here's what we can do. console.dir person. This dir is actually a slightly more advanced version of console.log. We just passed person. We didn't call it. So, what happened? If we look in the browser console, we get an object. Functions are actually nothing special. In JavaScript, functions are just another kind of object. So, just like person is an object, functions are also objects. So, we can create objects this way, too. We took the behavior of this person object, meaning all kinds of properties and methods, and put it into a function. Now, if you want to create a real person, what do we do? Let's say, the person's name is Messi. Then, we can write Messi is equal to person and pass Messi and 38. That means, we are calling the person function and passing Messi as the name parameter and 38 as the age parameter. So, by doing this, we are creating a new person named Messi. Again, if you want to create another person, let's say Neymar, then we can write Neymar is equal to person and pass Neymar and 33. That means our Neymar is created. So, we haven't learned anything new yet, but I just showed you how to create a function, return an object from inside it, and use that function to create real life-like objects. But, now we can create many many people using this person function. But, is there any problem here? Can you guess? Well, when we have only two persons in our program, it's not a big deal. But, in larger applications, creating so many objects will fill up memory. So, if there are countless persons like this, and we create each one separately with its own methods, performance will be affected in large applications. So, to avoid that, what you can do is share the methods. That means the methods that are common in our person function, the methods both Messi and Neymar shared, like eat and sleep, can be kept in a separate common object. Then, when we create person objects, we can reference those methods. What I'm trying to say is, when we create Messi and Neymar, we are creating everything twice. That means, when the person function is called for Messi, eat and sleep are created again. Then, when it's called for Neymar, eat and sleep are created again. In one word, the body of the person functions run every time person is called. But, we can avoid that. So, above, I wrote a new variable, person methods is equal to blank. I created an object called person methods. Inside this, we'll keep the methods we want to share, the common methods. In our case, person mainly has eat and sleep. I copied these two methods and moved them into the person methods object. Using shorthand method syntax, we wrote in person methods. So, now we have two methods inside a separate object, and those methods are in person methods. Now, what can I do? Inside the person function, instead of creating the methods directly, I can reference them from person methods. We know that when we reference something from an object, nothing new is created. The methods in person methods won't be created again, because they are already there. I can just reference eat and sleep from inside the person function, like this, earlier inside the person function body, we had person.eat as a function. Now instead, I will write person.eat is equal to person methods.eat. I referenced eat from person methods. And similarly for sleep, I will write person.sleep is equal to person methods.sleep. So, person now references person methods.sleep as well. What does that mean? No matter how many people we create now, all of them will use the same eat and sleep methods. Person methods is just one object. It isn't being recreated again and again. So, a lot of memory is saved. Now we are not recreating these methods repeatedly. So, it looks like all problems are solved, but still one problem remains. Can you guess what it is? If we introduce another method in person methods, let's say play, and write inside it console.log person is playing, then we would have to go back to the person function and reference that play method there, too. That means we have to change code in two places. That's a bit annoying, because we are basically working inside the person object, and every time we need to reference from another separate object. That's definitely not nice approach. We can see that clearly. So, what can we do? We don't want this. We want person to be able to automatically access whatever methods exist in person methods, whenever it needs them. To do that, we will use a JavaScript feature called object.create. We already created objects using functions. Now, we can also create objects using object.create. If you don't know object.create, no problem. I will explain it. First, let's see how object.create works. For understanding, we'll take a simple object. Captain is equal to a blank object. Now we created an object named captain. Let's add some properties to it. Name Messi, age 38, and country Argentina. Now let's create another new object. We will write player is equal to object.create captain. Here, using object.create, we created a new object in the player variable based on the captain object. That means using captain, we created a new object called player. That's what object.create does. So, how is this actually working? We did object.create and now if we log player, let's write console.log player, what do we get in the log? We get a blank object. That means there's nothing inside the player. But I'm saying I created player from captain. So, what's the benefit then? I'm still getting blank object. Okay, yes, it looks blank, but if I write console.log player.name, now look, the output is Messi. Player is blank, but when we access player's properties, it's getting the properties from captain. Similarly, if I write player.age, I get age two. So, even though player looks empty, we can still access values through player. That means the child object can access the parent's properties. But how is that happening? Player is actually blank. This is object.create's behavior. JavaScript understands that when we use object.create, player is the child and captain is the parent. When we write player.age, JavaScript first checks whether age exists in player. It doesn't, so it looks in the parent, that means captain. That's why we can access captain's properties through player.name or player.age. It checks whether captain has age, sees that yes, age exists, and then gives that value as the default. In other words, JavaScript is doing something extra smart here. This extra smartness was designed to make the programmer's life easier, but it also creates confusion sometimes because the behavior feels a bit unexpected. We think, "Bro, player logs as blank, but when I access properties, I get values." Yes, that is exactly object.create's behavior. But how does it work? This is possible because of prototypes. Object.create behaves this way because of prototypes. For now, just remember this much. With object.create, a child can access the parent's properties. We still haven't fully learned prototypes. I just introduced the name. We'll see how it works later. Now, let's go back to the previous example. Inside the person function, earlier we wrote person is equal to blank, a blank object. Now, instead of that, we'll write object.create and pass person methods. Here, person methods will be the parent of person. So, now what can we expect? The person object will automatically be able to access all methods in person methods, and we won't have to reference them separately anymore. Now, assume the code below doesn't exist. Just below the object.create line, let's write console.log person. Then we'll get a blank object. If we check in the browser, yes, we really get a blank object. It's showing a blank object twice because earlier we called person twice for Messi and Neymar. So, it logs twice. Even though person is blank, properties like person.name and person.age don't exist in the parent, but person.eat and person.sleep, person.play can automatically come from the parent. That means person methods. And we don't need to write person methods.eat separately anymore. As we saw earlier, we no longer need to write person.eat, person.sleep, person.play separately inside the person function. So, those lines can be removed. Now, after creating Messi and Neymar objects, if we write Messi.play and call it like that, the output comes. It works fine. For Neymar, too, if we write Neymar.play, that also works fine because according to object.create's behavior, person methods is the parent, and the child can access the parent's properties. That's why play is also working. So, the previous problem, having to write in both the person function and person methods, is now solved. But, we can improve this code even more. What I have shown so far is just a warm-up. What you saw is actually a very familiar programming pattern. Properties are shared from parent. Child can access those properties, and it can also add its own properties. But, for such a simple pattern, we are creating a separate object just to use as the parent. Meaning, we need help from another object outside person. That can feel a little inconvenient. There should be something built in for this, and JavaScript does have it. In a popular language like JavaScript, when we create objects, the parent's properties should be easily available to the child without needing a separate object reference. JavaScript has exactly that option. This is where prototypes come in. Now, we will understand what prototypes are. Instead of starting with a theoretical definition, let's first see an example. Let's write a simple function. Function test. So, we created a function called test. It does nothing, completely blank. We know functions are objects. Even though test does nothing, if we write console.log test.prototype, look, we get something. We can see the test function's body and inside it we can see prototype as a property. And just write console.dir test. Then we can also see that there's a property named prototype inside the function body. When we check test.prototype, we can see constructor function. Prototype is actually nothing mysterious. What we're seeing here, that is the prototype. So, if someone asks you in an interview, what is prototype, you can simply say prototype is a property of any function in JavaScript that points to an object. That is the definition of prototype. See what happened? We built the concept of prototype ourselves through examples. Now, let's go a little deeper. Remember the earlier object.create example? When we use object.create, the child can access all parent properties. We don't need to assign them separately to the child. Our earlier problem was that we had to create a separate person methods object to keep shared methods. But now remember, every function already has a prototype object. JavaScript gives us this built in. That means inside the person function, there is a prototype where we can keep shared methods. So, what we want to do is instead of creating a separate object like person methods, use person function's own prototype. Keep everything inside the person function. Use the built-in feature and share methods without a separate object. What I will do is I copied everything from person methods and now person methods is no longer needed. So, I removed it. Since we know the person function has a prototype property, I can write person.prototype is equal to a blank object. Then I'll put the methods that were in person methods into prototype. So, I'll paste it in. Just like before, we used object.create inside the person function. Now, where is person methods? We moved it into person.prototype. So, instead of person methods in object.create, now we will use person.prototype. In simple words, we just changed the name from person methods to person.prototype. And person.prototype is the person function's own property. So, now we can write person is equal to object.create and pass person.prototype. Now, what's happening? Person.prototype is the person function's own property, and we are storing the shared methods there. So, according to object.create's behavior, the child can access all those methods. Here, the child is the small person object. As a result, when we create Messi or Neymar, calls like Messi.play or Neymar.play still work. That means the improvement we made compared to before is we are now using prototypes. Like I said, prototype is nothing special. It's a built-in property of functions that JavaScript gives us. In that prototype, we can keep the things that need to be shared commonly, and the child can access them. And yes, we are getting the same output. That's fine. The reason I explained the story in such a roundabout way is so that it's easier for you to remember. Maybe after some time, if you practice this a few times, prototypes will become completely clear. The summary is any function has a prototype property. Whatever I put in that prototype, the child can access it. I could have said that in the beginning, but then you wouldn't understand the context. You wouldn't understand why prototypes are needed. Now, you can feel how valuable prototypes are. This is why JavaScript is called a prototype-based language. Prototype is not a new feature. It's not some new addition in modern JavaScript. Prototypes have existed in JavaScript from the very beginning. JavaScript is built on prototypes. Built-in languages like C or Java, or the languages many of us studied academically, there's no such prototype concept. Those who have worked with class-based languages like C++ or Java will understand this easily. Even if someone comes from PHP, they know PHP now also has classes for object-oriented programming. The real thing is classes work in a similar way. What we saw in the earlier example is basically class-like behavior. But JavaScript didn't originally have classes by default. Vanilla JavaScript had no class feature. That's why JavaScript uses prototypes, so child objects can inherit parent properties. Inheritance means just like we get genetic traits from our father. In the same way, a child object gets traits from a parent object. In other languages, this happens through classes, but in JavaScript, inheritance happens through prototypes. That's JavaScript's difference. That's why prototypes may sound like a huge topic, but prototypes are actually very simple. It's a property of JavaScript functions that helps with inheritance. Whatever I put in prototype, the child object inherits all those traits. That's really what it is. Look, person. Person is a function or object. Whatever traits are inside this function's prototype, the small person, meaning the child object, inherits all those traits from parent person.prototype in the same way. In our example, there are only methods, but if there were other properties, too, the child would inherit those as well. Everything comes to the child automatically. Now, we don't need to define any third object, and we don't need to create extra things ourself. Using built-in prototypes, we can access properties from parent to child. If this much is clear, then let's go a little deeper. The fun starts from here, and then you will also understand why I explained everything in such a roundabout way. From what we have seen so far, we learned three things. First, how to create a constructor function. The person function is called a constructor function because through this function, we construct an object like Messi. There's nothing complicated there. By default, all functions in JavaScript are constructor functions. We can use them easily to create objects. The convention for constructor function is that the name starts with a capital letter. That's why we identify person as a constructor function. Second, we learned how to add methods to a constructor function's prototype. Whatever methods we add to person constructor's prototype become shared across all instances. Third, we learned how object.create helps the child inherit the parent's properties. These are the three things we learned so far. And the whole purpose of this process was to share common methods among all instances of the constructor function. Using the person constructor, we can create Messi, Neymar, and many more person instances. And we went through all these steps just to share the common methods among those instances. For anyone who works with programming, this is a very fundamental concept that the parent constructor function's properties and common methods can be shared with the child. This is basic. So, naturally, there should be some simple built-in shortcut for this. And JavaScript has exactly that. What we did earlier, creating instances through the person function, we can do that using the new keyword. For example, in the previous code, we created Messi and Neymar like this. Messi is equal to person, and we passed Messi 38, and same with Neymar. Let's comment those out. Now, we write it in a new way. Messi is equal to new person, and we will pass Messi and 38. Same goes with Neymar. So, yes, by putting the new keyword before calling the person function, we can do the same thing. What's happening here? We are creating a new person named Messi, whose age is 38. Meaning, we passed the required data. In simple words, we are creating a new person named Messi. So, we can say, earlier, we were just calling the function, but now, by using the new keyword, we are getting some extra benefits. I will explain that now. Now, let's tweak the example a bit. Instead of using the previous person function, I'm creating a new function, so we have two separate examples, and it becomes easier to understand. We are changing the function name, so it's different from the previous person. Now, we will write new person with new and pass Messi and 38. And same with Neymar. So, we changed the function name from person to person with new. Now, we also need to create this function body. We copied the entire body of the previous person function exactly. Just change the name. Earlier, it was person. Now, it's person with new, because below, we we calling person with new. We also copied the earlier person.prototype and changed it to person with new.prototype. And we also used person with new.prototype in object.create. I'm doing all this just to show you the difference between the two because there actually is a difference. In the earlier person constructor function, we were basically doing two things. One, we were creating the main object with object.create. Without that, the object wouldn't be created. And finally, we were returning the object so that we could use this person object. So, we were doing these two things manually. But when you use the new keyword, JavaScript automatically tries to reduce the programmer's work. When using the new keyword, we don't need to write separate lines for object.create and return person. JavaScript assumes those two things for you. That means you don't have to write those lines manually anymore. Is that just convenience? No, it doesn't stop there. JavaScript is also saying, "Since you don't need to write those two lines, let me make this even easier." So, JavaScript itself internally writes the object.create and return lines. I commented out the object.create and return person lines. Now, think about this. Earlier, I wrote object.create and passed person.prototype. When JavaScript does this internally, it doesn't know or care about the variable name person. It won't use the name we gave. JavaScript uses this instead of person. This, t h i s, and it also returns this automatically instead of person. That means when we used to create let person manually, JavaScript now creates it internally as this. Just keep this much in mind for now. This behaves exactly the same as before when we created objects by calling normal functions. Back then, we had to manually write let person is equal to object.create. But when we write new person with new, JavaScript itself internally writes the let person and return person lines. So now, we can assume those two lines already exist. We only need to assign the properties. Those who have worked with React JS class components have seen that inside constructor functions, we directly use this. But we don't manually write object.create. JavaScript works the same way internally. React also uses this internally, and we assign properties easily using this. So, now in person with new, person.name won't exist. JavaScript is basically saying, write it using this. this.name is equal to name, this.age is equal to age. This is how the values will be assigned. So, now it's clear that when we create instances with new, we don't need to manually write Object.create and return statement. We can directly assign properties using this. And we can also add new properties easily. For example, we can write this.contact is equal to something. So, this way, if we want, we can add more properties, too. Now, we added a new contact property. Now, according to the workflow, we won't keep the previous person with new name. We will rename person with new back to person. We will also change person with new.prototype into person.prototype. And we'll update all function calls below accordingly. We'll just use person instead of new person with new. Now, let's see if it works. If we do messy.play, can we access it? Let's check in the console. Similarly, Neymar will also work fine. So, this is our journey. JavaScript now supports classes, too, and prototypes play a very important role here. So, now let's convert everything we have done so far into a complete class. If we use the familiar class syntax, we won't have to deal with prototypes directly anymore. So, let's convert the programs we have written so far into a class. To make it easier to understand, we'll keep the person constructor function at the top for now, so you can compare the two side by side. We already know person is a constructor function. A class also has a constructor function. We'll name the class person as well. So, we write class person. We are using the same name as the person function. Inside class person, there will be a constructor function. This constructor takes two parameters, name and age. Inside a class, we write the constructor like this. Constructor, and pass name and age. And inside the constructor, we can use this. First, we assign the name parameter. So, this.name is equal to name. So, the name we receive in the constructor is stored in this dot name. Then, we add age. This dot age is equal to age. With that, the constructor's job is done. The functions that were in the prototype can now be written directly inside the class as methods. We have copied all three functions into the class. Since those functions had commas when they were inside an object or prototype, we remove the commas when writing them inside the class because classes don't use commas between methods. That completes our class. Now, we can remove the original person constructor function and person.prototype because we don't need them anymore. At this point, we can clearly see the comparison, right? Finally, we write Messi is equal to new person and pass Messi and age as 38. And Neymar, same. Then, we call Messi.play and Neymar.eat. If you check the output in the console, you'll see the exact same result as before. Whatever output we are getting earlier, we are getting the same thing now. So, this is how we converted prototype-based code into modern class syntax. So, by understanding prototypes, we learned a lot of things. We saw how objects can be created. We learned three ways: with functions, with Object.create, and with the new keyword. Finally, by coming out of prototypes, we were able to transform our code into something that looks like a complete object-oriented programming language style, that is class syntax. What we saw so far was the fundamental concept of prototypes, how prototypes work. Now, I will show you a few optional things. You can skip this if you want, but I want to show it so you can see a real example and feel it more clearly. Let's move away from our boring example. When we create arrays in JavaScript, we usually write persons is equal to a blank array. This is actually equivalent to let persons is equal to new array. Here, new array is the constructor, and the third bracket is array literal. Because writing new array repeatedly is a bit annoying, JavaScript gave us array literal syntax for convenience. This kind of thing exists in many other languages, too. So, yes, we usually use third bracket to create arrays, but for convenience, let's look at new array for a moment. Just like in the previous example, we initialized a new person using new person. Now, we'll do the same kind of initialization with array. Now, even before accessing any custom property, the array already gives us a lot of methods and properties. For example, we write persons.push and we'll pass Messi. Here, we pushed Messi into the array. Then, if we write console.log persons, we can see Messi inside the array. But, where did the push method come from? Because persons is our own variable. Persons is a new array. So, where is push coming from? Just like in the previous person example, we said, "Whatever exists in the prototype, the child inherits those." In this case, the child is persons and the parent is capital array. If we write console.dir and pass the capital array, then we can see a prototype inside it. Now, let's inspect that prototype a bit. It will be easier to understand. So, let's write console.log array.prototype. Notice this. We just used push and push is right there inside prototype. So, that means this exactly matches our earlier example. Like I said, whatever methods are in the parent's prototype, the child can access them. That's exactly what's happening here, too. Capital array is the parent and the methods are in its prototype. That's why the child can access those methods. Through this example, I wanted to show how prototypes work in a real built-in example. I hope this made it much clearer. Many times when you work, we just think of this as built-in features. Like array.push just works because JavaScript understands it. But, why does it work? It works because of prototypes. When we create any object, it inherits the properties and methods that exist in its parent's prototype and all of those become accessible to the child as well. This is actually called a prototype chain. Chain is a very simple idea. Whether we write new array or create a new object, the same idea is working underneath. If you understand prototypes properly, then you can really reach boss level in JavaScript. In today's video, I'm going to talk to you about object-oriented programming. When JavaScript developers hear the term object-oriented, they suddenly feel sick like something very complicated. Some even get scared of it. But honestly, it's not that difficult. My main goal in this video is to explain object-oriented programming in such a way that even a beginner or even a young kid can understand it easily and get a completely clear idea about it. So, without wasting any more time, let's get started. Let's get straight to the point. What exactly is object-oriented programming or OOP in short? As humans, we naturally think of everything around us as objects. But when it comes to writing code, we often forget to look at it that way. Especially, those of us who are using scripting languages like JavaScript or Python tend to think in a functional style. Meaning, we try to solve everything through small, separate functions. That's because these languages were originally designed with a stronger tilt toward functional programming. But that doesn't mean object-oriented concepts don't work here. If we want, we can absolutely apply object-oriented principles in JavaScript or Python as well. But if we look at some popular programming languages like Java or C++, these were specially designed with object-oriented programming in mind. However, even in languages like JavaScript and Python, you can still make use of object-oriented concepts if you want. Let's understand this with a simple example. Suppose you need to build a soccer game. The game will have different players. Now, if you want to define a football player in code, in JavaScript, you can create an object that contains properties like name, age, and skill. If there's only one player, say Ronaldo, then it's not a problem. You can easily represent him with a simple object. But, when you need multiple players like Ronaldo, Messi, Neymar, you would have to define each one as a separate object. And that's where a big problem appears. All three players share almost the same object structure. Yet, you are writing them out separately. If that number grows, say 100, 200, or even 300, you'd be manually writing each one every single time. The first problem is you end up writing duplicate code. When you repeat the same thing over and over, the chances of mistakes go up. For example, while creating Ronaldo's object, if you accidentally type s i k w l instead of s k i w l, it can be hard to catch. As you write hundreds of such objects, these kind of errors will increase and introduce unnecessary bugs into the code. This is exactly where object-oriented solves the problem. Now, imagine someone suddenly tells you to add a new property to the player object. Let's say experience. If your code already has a hundred player objects, you'd have to find all hundred and add the experiences property to each one manually. That's obviously tedious and time-consuming. And that's a major issue. These are exactly the kinds of problem object-oriented programming was created to solve. If we had thought in an object-oriented way from the beginning, we wouldn't face this hassle because there's one common factor here. They're all soccer players. Ronaldo, Messi, or Neymar. They may be different individuals, but their core identity is the same. They are all soccer players. That means there should be a generic or common structure for all of them. And from that structure, each one will be created individually. And this common structure is what we call a class. The foundation of object-oriented programming is actually built on this very concept of classes. So, as we just saw, when creating individual objects, the properties of each one are basically the same. Now, if we want to bring all of that into a common place, we will need something like a factory, a system from which all players will be created. Imagine we have a player factory. All we will do is provide that data. For example, Ronaldo, age 35, skill striker, experience 10 years. For Messi, the name, age, and skill will be different, but the properties will still remain the same. In other words, even though the data is different, the structure or characteristics remain the same. And this is exactly what the concept of a class comes from. A class is the blueprint or design from which objects are created. For example, suppose we create a class called player. Inside this class, we will have common properties like name, age, and skill, but it won't contain the details of any individual player. It will only define the structure. Then, using this blueprint, we can create Ronaldo, Messi, or Neymar whenever we want. So, what exactly is a class? A class is basically a common structure or a factory where all the properties are predefined. And when we write new player, a new player is created using that blueprint. For example, if we write new player, it will create a new object where the name will be Ronaldo, the age will be 35, and the skill will be striker. Notice one thing here. The player class itself is not Ronaldo. Instead, it's the common structure from which many players like Ronaldo, Messi, or Neymar can be created. And only when we call new player, that's when the actual Ronaldo object is created. This way, our code becomes much cleaner and easier to read. Now, imagine we need to create hundreds of players like before. We no longer have to write duplicate objects over and over. All we need to do is call new player. And a new player will be created instantly. And if there's ever a bug, it will be much easier to find because all the properties are defined in one place, inside the player class. Now, let's talk about the constructor. When we write new player in the bracket, Ronaldo, {comma} 35, {comma} striker, the arguments we're passing here are actually the arguments of the constructor function. A constructor is a special function that lives inside a class, and it gets called automatically whenever a new object is created. That means whenever we use new player, JavaScript automatically calls the classes constructor function. And the arguments we pass in, like Ronaldo, uh 35, striker, get assigned inside the constructor to this.name, this.age, and this.skill. So, later, we can easily access them as Ronaldo.name, Ronaldo.age, or Ronaldo.skill. Simply put, a class is the blueprint, and the constructor is the machine that builds the actual object from that blueprint. When we call new player, the arguments we pass go directly into the constructor function inside the class. For example, if we call new player, Ronaldo, 35, striker, these three values, Ronaldo, 35, and striker, are passed as parameters to the constructor function, which in this case is expecting name, age, and skill. So, what does the constructor do? It takes those arguments and initializes them inside the object. Meaning, it sets them using this.name is equal to name, this.age is equal to age, and this.skill is equal to skill. Here, this actually refers to the object that's been created at that moment. For example, if we call new player with Ronaldo, 35, and striker, then this refers to the Ronaldo object. If we call new player Messi, 36, and striker inside the same constructor function, this will now refer to the Messi object. In other words, this is a generic reference that always represents the specific instance being created at that moment. To put it more simply, this works like a nickname. Just as you might call your child by different names, baby, sweetie, or cutie, no matter which name you use, you are still referring to the same child. In the same way, this is a reference that always points to the specific object being created each time a new instance is being made. So, the whole process works like this. Number one, you write new player Ronaldo, 35, striker. Number two, the constructor is called automatically. And number three, the constructor takes the arguments and sets them to the properties of this object. And finally, number four, a new object is created, which gets stored in the Ronaldo variable. Now, when you call Ronaldo.name, you will get Ronaldo. If you call Ronaldo.age, you will get 35. And calling Ronaldo.skill will return striker. This is how the constructor works automatically whenever we use new to create an object from a class. And this newly created object is called an instance. That means Ronaldo is an instance or the real form of the player class. In the same way, you can create as many instances as you want. Messi, Neymar, Ramos, each will be a separate instance of the player class. So far, we have seen how the default constructor method of a class works, but it doesn't stop at the constructor. You can also write your own custom functions inside a class. When a function is written inside a class, it's called a method. It works just like a normal function, but since it lives inside a class, it gets a special name, method. For example, let's say we add a method called say hello. Now, when you create an instance with new player Messi 35 and striker, along with the name, age, and skill set inside the constructor, this new say hello method will also be available in that object. So, you can directly call Messi.sayHello. Since Messi is the main object or instance, you will be able to access that method through it. And when you call it, you will get a nice output in the console, like "Hello, my name is Messi and I'm a striker." In other words, methods represent the actions or behaviors of an object. For example, a player object's properties like name, age, and skill define its characteristics, while a method like say hello defines its behavior or how it acts. You can add multiple methods if you want, such as play, retire, or show stats. This way, object-oriented programming doesn't just keep an object limited to storing data. It also gives the object the ability to perform actions. I hope this made sense. So, let's keep one thing clear from the very beginning. A class is basically the design or blueprint of of object. Thanks to this blueprint, we can avoid writing duplicate code, reduce the amount of code we need, and if we ever want to add a new property or characteristic in the future, we can do it in just one place. That's why OOP is considered elegant, and it's widely used in building large-scale software. This is the basic idea. Now, let's look at an example. Suppose your game has a class called human. Inside it, there's a constructor that will only take a name when creating an object from outside. Every human has two hands and two legs. These are fixed and are same for everyone. So, they can be treated as a static properties, and the name will come from outside when the object is created. Now, you will notice that just the human class isn't enough because in your game, there are different types based on age, like baby and teenager. If you define a baby class, it will also have a name, two hands, and two legs just like a human, but it will also have its own unique property. Let's say a baby is very cute. That can be stored as a separate property. Along with that, a baby will have its own method, cry, since babies cry. In the same way, you have another class called teenager. Just like before, it has the same three common things, name, arms, and legs. These three actually come from the human class, but a teenager also has unique characteristics. For instance, being emotional, and because of that, teenagers often show a distinct behavior, like getting a crush when they see someone attractive. We can define that as its own method. Now, if you look closely, both the baby class and the teenager class have name, arms, and legs. That means the same code is being duplicated. To To this kind of repetition, object-oriented programming uses a special concept called inheritance. The word inheritance means receiving something naturally from ancestors or parents. Just like in real life, you inherit many traits from your parents. For example, if your parents are very intelligent, you likely inherited that to some extent. Or let's say your father is tall or short, you inherit that physical trait through DNA. In the same way, in programming, a child class automatically inherits the common properties and methods of its parent class. So, now let's put that concept into practice. Suppose your main class is named human, and then you create another class called baby. To quickly build baby from human, we write class baby extends human. Here, extends is a keyword that allows the baby class to inherit from the human class. This means whatever features the human class has, the baby class automatically gets them, too. That's how inheritance works in code by using extends. So, when baby extends human, it inherits all the common properties like name, arms, and legs. On top of that, baby adds its own unique property, which is this.cute. Exactly, the baby class also has its own special method called cry. This is how one class can take properties from another and form a new one. And this whole process is what we call inheritance. In the same way, the teenager class will work just like this. It will inherit the common features from human, but it will also have its own unique property, emotional. And its own method, crash. I hope this part is clear now. Now, let's move on and see how to actually call or create these objects. So, we have baby class, and it's extended to human class. Now, when you want to create a new baby object, you might write const Monty is equal to new baby Monty. This would create a new baby object and set its name to Monty. But be careful. Written like this, the code won't work straight away. The reason is inside the baby class, we never initialized this.name, but the name property has originally defined in the human class. When we create a new object, only the baby class constructor is called automatically, not the human classes constructor. So, how will the human class know that the name passed into baby's constructor should also be set as the parent human's name? To solve this, we need to call the parent classes constructor from the baby classes constructor. And for that, we use super method. Whenever you write super name inside the baby classes constructor, that name value gets passed to the human classes constructor. And human takes care of initializing it properly. Exactly, that's the point. From the child class, if you want to send values to the parent classes constructor, you have to use super method. Without it, the parent class would never know that it needs to receive the value for name. So, what you need to do is simple. Just call the super method. For example, when you call new baby Monty, the string Monty first comes into the baby classes constructor. Now, since baby's parent is human, that name parameter must be passed up to human. Otherwise, the this.name, this.arms, and this.legs inside human won't get initialized. To fix that, we call super in the bracket name. When you place super name right above this.cry in baby's constructor, what actually happens is the human class's constructor gets called, and it takes care of initializing those parent properties properly. So, I hope I have been able to give you a clear idea about inheritance. Object-oriented programming has many more ideas beyond this. For example, apart from the methods we have looked at, there are also static methods, private methods, and private properties. Not only that, OOP is built on four main pillars. I only discussed inheritance here, but there are also abstraction, polymorphism, and other important concepts. If you would like me to cover those in detail, and if you feel this tutorial helped you understand things easily, then do let me know in the comments and request more videos. Also, share how you found this video. If you enjoyed it, don't forget to like and share. If I get good responses from you, I will keep making more videos like this. I hope this video has been useful for you. Thank you, everyone. Today, I'm super excited to share another important JavaScript concept with you, event propagation technique. We will dive into event bubbling and event capturing. Earlier, I made a video on event delegation, which is actually a part of event propagation. That topic needed a separate video, so you will find the link to that in this video description. By the end of this video, you will understand how bubbling and capturing work, and be ready to tackle some tricky interview questions related to them. Let's get started. Let's begin with a simple example. First, what is event propagation? The word propagation means spreading or moving from one place to another. It comes from the English verb propagate, and propagation is the noun form. Simply put, it's about moving up, down, or spreading along a specific path. We already know how events work in JavaScript, but the question is, in what order do these events run? That whole process is called event propagation. To make it clearer, let's look at the example in the browser. You can see a button inside a form, and that form is inside a parent div. The green part you see is the form, and the purple border around it is the parent div. If you look at the index.html for this example, you will see there's a div, the parent, inside it a form, and inside the form there is a button. The styles you see are coming from a style.css file. I will share the code on GitHub, so you can check it out if you want. That's our current setup. Now, if we add separate event listeners to each of these three elements, one for the button, one for the form, and one for the parent div, when you click the button, the event should fire in all three places, right? Because the button is inside the form, and the form is inside the parent div. So, which order do the event listeners run in? Does it go from child to parent or the other way, from parent to child? This whole process is called event propagation. In other words, the order in which events run is what we call event propagation. That's the main definition. Event propagation has three parts: event bubbling, event capturing, and event delegation. I have already made a video about event delegation, and you will find the link in the video description. Today, we'll dive into bubbling and capturing. Let's say our script.js file is attached to index.html. In script.js, I have selected the three elements, the parent div, the form, and the button, using document.getElementByID. Each has a click event listener function, and all of them use a common function as the listener. So, when you click the click here button, you will see do something logged three times in the console. Now, which output comes first and which comes later? To figure this out, you could use separate listeners. But, since we are using a common function, we need to use the event object. Inside the event object, there's event.target property, which tells us which element was actually clicked. So, console.log event.target lets us know the real target element. If you watched the previous event delegation video, you will understand this part even better. There, we saw that event.target gives us the HTML element that was targeted. Now, if we refresh the browser and click the click here button, you will see the button element logged three times. That's natural, because target means where the click happened. Since you clicked the button, it shows the button each time. But, the interesting thing is when you click, not only the button's listener fires, but also the form's and the parent div's listeners fires, also. Yet, the target is always the button in all three cases. So, the output looks the same. If you want to know which listeners are being called, you need to use event.currentTarget. Now, if you click the button again, you will see first the button, then the form, and the parent div. So, the event is moving from child to parent. Since we didn't configure anything, the events are running in their default behavior, moving from child to parent. First the button, then the form, then the parent. This default order is called event bubbling. What does bubbling means? The word comes from bubbles. Think about what happens when you blow soap bubbles. They rise from the bottom to the top. In the same way, events move upward. That's why it's called event bubbling, right? Now, we clearly see that event.target and event.current target aren't the same. This is a common interview question. For example, if you don't use event.current target and just log this in the console, what happens? Let's check. See, this actually returns the same element. So, inside an event listener, this refers to current target. That means this is equal to current target. Let's understand what current target means. Current means the one that's active at the moment. For example, when you click the button, the main target is the button. So, in all three cases, target is the button. But current target changes step by step. In the bubbling process, the event is first on the button. So, current target is the button. Then, when the event propagates to the form, current target becomes the form. Finally, when it reaches the parent, current target is the parent div. This current target is what you get as this inside the event listener. So, if you get asked in an interview, you can simply answer this and event.currentTarget do the same thing. You can even get different information from current target. For example, current target.className gives you the class and this.tagName gives you the tag name. So, when you click the button, the output will be button, then form, and then div, just like in our markup. Isn't that cool? Now, we clearly understand how event.currentTarget, event.target, and this work together, even though they are different. We have also got a good grasp of bubbling process. So, that's great. Let's see what happens if we want to reverse the default bubbling flow. By default, the event goes from bottom to top. But what if we want it to go from top to bottom? It's simple. You need to use the third parameter in the event listener. In this third parameter, you can pass an object with a property called capture. By default, capture is false. If you want the event to go from top to bottom, set capture to true. Now, when you click the button in the browser, you'll see the parent div fires first, then the form, then the button. So, the default order is reversed. This process is called event capturing or trickling. It's the opposite of bubbling. So, we have seen two types of propagation, default bubbling and capturing or trickling. That's also event delegation, which I have already shown in a previous video. Finally, here's a fun fact for job interviews. Sometimes, the order of your events can be random. Suppose you have a requirement and you have removed the third parameter. In the interview, you are told when you click the button, the form's listener should fire first, meaning the button's immediate parent comes first, then the button's listener, and finally the parent div's listener. You might think by default bubbling means the button fires first, then the form, then the parent. But here, they want the form first, then the button, then the parent. So, what should you do? Remember, if you set capture to true on all elements, then by default capturing will make the parent fire first, then the form, then the button. But we want the form first, button second, and parent at last. So, setting capture to true on all won't work. You need to think step-by-step. Set capture to true only on the form's listener. Keep capture false on the button's listener and also keep capture false on the parent's listener. If you set it up this way, the form fires first, then the button, then the parent. Got it? The key is to bring the button's immediate parent into the capturing method and keep the parent div in normal bubbling. So, just set capture to true on the form's listener. Understand that by default, when you click the button, the button fires first, then the form, then the parent. But if you set capture to true on the form's listener, the event fires on the form first, then the button, and the parent div's listener fires according to bubbling. So, first the form, then the button, then the parent. Choose the order you want. Because capturing is applied only to the form and not to the parent. So, the parent follows bubbling. This is a question that often comes in interviews in different ways. But if you know the step-by-step process and fully understand bubbling and capturing, you won't have any trouble. I hope this example helped you see how it works. Here's a final trick. Sometimes there's a shortcut for the third parameter. Instead of passing an object, you can just write true. By default, it's false. If you only want to set capture to true, just use true directly. But if you want to set more options, then use an object. You can check the documentation for all the options. To sum up, today we learned about event propagation and its techniques. Bubbling, capturing, also called trickling, current target, this keyword, you now know all of this clearly. I hope this video was helpful for you. If you get asked in a job interview, what is event propagation? What is event capture? What is event bubbling? What is trickling? What is current target? What is this? You will be able to answer confidently. If you enjoyed this video, don't forget to hit the like button. Drop a comment to let me know your thoughts and share it with your friends so they can get benefited too. It really helps me bring you more value-packed tutorials. Thanks for watching and I will see you in the next one. Today, I'm super excited to share an important JavaScript concept with you. Event delegation. This is a topic that often comes up in job interviews. The name might sound a bit complicated, but once you see the implementation, you will realize how simple it actually is. By the end of this video, you will be confident enough to explain event delegation clearly in any job interviews. So, let's get started. Let's begin with a simple example. The word delegation means to assign your task to someone else. That's where the idea of event delegation comes from. In JavaScript, we know about events and event listeners. For example, when you click on an element, we can catch that event with an event listener and respond accordingly, right? We all know this. So, what exactly is event delegation? Normally, we attach an event listener to a single element and perform actions based on its events. But, the real meaning of delegation is using just one event listener for multiple elements. Imagine you want something to happen when you click on this JavaScript element and something else when you click on this PHP element. To do this, you would usually add separate event listeners to each element. If you have five elements, you would need five different listeners. And event delegation is useful in situations like this. Suppose your list data comes from an API or you don't know how many list item you will have. Maybe you have a button that adds new elements to the list when clicked. In these cases, you would need to create event listeners dynamically, right? That's where event delegation comes in handy. There are two main things to understand here. First, we use a single event listener for multiple similar elements. And the second is, even if new elements are added dynamically to the list, that same single event listener will still work. Let's see these two concepts in code. Here's the HTML page we'll be working with. There's already a script.js file created and we will write our code there. Looking at the UI, the list you see is an unordered list or UL. If we were to add event listeners to each LI item separately, what would we do? We would give each LI a unique identifier and add an event listener to each one, ending up with five separate event listeners. But, we are not going to do that. Instead, we will use just one event listener. Simply put, we will apply the event listener to the parent element of the list, not to each item individually. So, what's the parent here? It's the UL element. I I given the UL an ID of list. So, we can select it using document.getElementById list. After selecting it, our next step is to add an event listener to this list. Let's listen for click events. In the callback function, we'll get the event object. For now, I'm showing that event in the console using console.dir. So, we can see what's inside. Please note that I'm using console.dir in place of regular console.log for more detailed login. Save the script.js, open the browser console, and click anywhere on the UL list. You'll see a pointer event object with lots of information. If you look closely, you'll find out which HTML element was clicked inside the target property. This means when you hover over the target property in the UI, that element gets highlighted. So, no matter which LI you click, you'll get the same kind of output. In other words, event.target contains the actual HTML element, in our case, the LI, right? If we change some styles, say, set the background color to yellow, it will apply directly to the LI. Now, if you go to the browser and click any LI in the list, you'll see exactly that happening. This is how we have implemented event delegation. We didn't work on each LI item individually. Instead, we added the event listener to the parent element, delegating the responsibility to the parent. That's event delegation. So, the first condition of event delegation is fulfilled. We are using just one event listener. Now, let me mention something else. Suppose you want to perform different actions on individual elements, not just the same thing for all. Yes, that's possible. You can do different things based on the target. Inside the event listener, we are using console.dir in the bracket e.target. That lets us see which element was actually clicked. If you move your mouse over the UL, you will see the whole area highlighted in green showing the parent area. So, if you click anywhere on the UL, the whole area turns yellow. But, we want the event to happen only when an LI is clicked. Simply put, we can check the target. If it's an LI element, the code will run, otherwise nothing happens. It's easy to do an if else check inside the event listener. What should we check? For example, we can write e.target.matches function in the if statement. If you don't know about the matches function, let me explain the matches concept. You can use the matches function on any HTML DOM element. element.matches returns true or false, meaning it checks if the element matches the given selector. For example, if we write e.target.matches and in the bracket LI, the code will run only if it matches an LI item. You can use any CSS selector in the matches function, a class, ID, or element type. Whatever you provide, the function works accordingly. Now, if you go to the browser, you will see that clicking on the empty part of the UL doesn't trigger the event anymore. Only clicking on an LI triggers the code. So, you have got a level of control. Now, let's say you want to give different colors to each element, not just the same color for all. For example, you want JavaScript to be yellow, PHP to be red, Java to be blue, and Python to have another color. That's easy to do. We only need the text inside each list item. To get that, we can simply use console.dir e.target in the bracket again. When we click on any LI, check the console. You will notice an inner text property. That property holds the text of the LI you just clicked. For example, if the LI's text is JavaScript, you can act accordingly. It's easy to do this in code. Just add a simple check inside the event listener. You can use a switch case or individual if else elements. Whatever logic you use, let's say you check e.target.innerText. If inner text is equal to JavaScript, set the background color to yellow. For any other element, set it to blue. So, if you click on the JavaScript li, it turns yellow. Clicking on any other li turns it blue. Now you have control. You can implement any logic you want, but the main thing is by putting the work on the parent element, you have delegated the responsibility. That's why it's called event delegation. The second condition of event delegation is that the listener works even when new elements are added. Now, let's add a new element. Suppose, below the ul, we add a div and a button. The button says add new element and calls the add element function on click. We haven't created the add element function yet, so let's do that now. In script.js, we need to create a function called add element. Inside this function, we will dynamically add a new element. That's easy to do. For example, we'll write new element is equal to document.createElement, a standard DOM method for creating new elements. Now, we have created a new HTML element, say an li. Then we set its text with new element.textContent is equal to Ruby. Next, we need to add this new element to our list. That's done with list.appendChild and in the bracket new element, which adds a new child to the end of the list. Now, if you look at the browser, you will see a new li called Ruby added to the UI. If you click on Ruby, you will see that the event listener is working still. This confirms that our event delegation is fully effective. From this, you can see that both conditions of event delegations are met. First, we are using a single event listener for multiple elements. Second, the listener works even when new elements are added, whether from an API or dynamically. If we had added separate listeners to individual elements, we would have to create new listeners every time a new element was added. But now, we have added one listener for the whole list, and it's working. That's a big advantage. You can control the entire list from a single place. That's the end of event delegation. I hope you will never be surprised if you are asked about it in a job interview. You will be able to explain what event delegation is and how it works confidently. If you enjoyed this video, don't forget to hit the like button, drop a comment to let me know your thoughts, and share it with your friends so they can get the benefit, too. Let me quickly mention our Logic Based Labs Facebook and LinkedIn pages. That's where we regularly share coding cheat sheets, design system guidelines, and tons of developer-focused content. If you want that kind of rich, practical value in your feed, go follow the page. You will find all the links, GitHub, Facebook, LinkedIn, everything, right there in the video description. Thanks for watching, and I'll see you in the next one. JavaScript was once called the most ugly language, but it has evolved significantly over time thanks to a rich and resourceful ecosystem. It is now considered the most popular programming language. Some even say it will eat the entire internet. But today, we aren't just here to praise it. The most important factor that transformed JavaScript into the powerhouse it is today is its asynchronous behavior. To work with JavaScript at an advanced level, you first need to understand this concept. In this video, I'll give you a clear idea of how asynchronous JavaScript works, so that you won't have any more confusion. By nature, JavaScript cannot perform multiple tasks at once. It runs one operation at a time, which is why it's called a single-threaded programming language. What does that actually mean? Let's imagine a restaurant scenario. Suppose a restaurant has two tables and one waiter. The waiter goes to table one, takes the order, and goes to the kitchen. Until the food is finished cooking, he remains blocked. Only after serving customer one, does he become free to go to customer two, take their order, and wait again. By default, JavaScript works exactly like this. The customers are the users, and the waiter is the thread. This is synchronous blocking behavior. It's called blocking because a thread stays idle while waiting for a task like cooking to finish. Let's see how this looks in code. Imagine a function called process order that takes a long time to complete. When JavaScript runs, it might first print take order for customer one. Then it calls process order. Inside that function, it prints processing order for customer one. But then it hits a while loop running for 3 seconds. While that loop runs, JavaScript cannot move to the next line. It stays blocked, just like our waiter. After 3 seconds, it finally prints order processed for customer one, and moves to the next line to print completed order for customer one. If a user encounters this in a real application, the interface would freeze, which is a terrible user experience. This is why asynchronous JavaScript is absolutely necessary. Before we dive into that, we need to understand how JavaScript runs inside the browser. When the code runs, it is converted into machine language by the JavaScript engine. While different browsers have different engines, like Chrome's V8, Internet Explorer's Chakra, or Firefox's SpiderMonkey, Google Chrome is considered the most powerful because of the efficiency of the V8 engine. Inside the browser, there are two main components: the runtime environment and the engine. The runtime environment provides objects like window, document, and set timeout. Inside the engine, the The important part for us is the call stack. JavaScript execution begins with a main function. As the code runs line by line, functions are pushed onto the call stack. For example, console.log is pushed. It prints to the console and then it's popped up. When it hits the process order function, that goes onto the stack. If there is a 3-second while loop inside, the call stack remains occupied. The browser is blocked. The user can't even click a button. This is the last in, first out or LIFO nature of the stack. Now, let's see how we can remove this blocking behavior by replacing that while loop with a built-in function called setTimeout. In this version, JavaScript behaves differently. When it reaches setTimeout, it recognizes it as an asynchronous function. Instead of waiting, it hands the task over to the web APIs, which is the part of the runtime environment, and immediately moves to the next line. While the 3-second timer runs in the background in a separate thread provided by the browser, the main thread continues executing the rest of the code. This is why you'll see other logs print instantly while the cooking happens in the background. Once the web API timer finishes, the callback function, that means the arrow function inside setTimeout, is sent to the callback queue. Here, the event loop comes into play. Its job is to watch the call stack and the callback queue. When the call stack is empty, the event loop takes the first task from the queue and pushes it onto the stack for execution. This is how JavaScript performs multiple tasks despite being single-threaded. It has assistants. This non-blocking approach ensures the user can still interact with the UI while heavy tasks happen in the background. However, this creates a new challenge, flow control. With setTimeout, we can predict the 3-second delay, but in real life, we deal with API calls, file system reads, or database queries, we don't know exactly when those will finish. If we can't control the order of execution, it becomes a problem because the sequence of tasks is often critical. Let's go back to our restaurant scenario. Now, the waiter takes customer one's order and tells the kitchen, "Call me back when this is ready." Then he immediately goes to table two. He isn't blocked, he is free. When the kitchen calls him back, he finishes the order. In code, we handle this using callback functions. By passing a function into another function, we can ensure that complete order only runs after process order is done. This works and it's non-blocking, but it leads to a problem called callback hell. As you add more tasks like complete order two, three, and four, the code nests deeper and deeper, becoming unreadable. To solve this, JavaScript introduced promises. A promise is exactly what it sounds like, a promise to do something. It can be resolved or fulfilled and rejected or failed. To use a promise, we use then for success and catch for errors. This is much more human-friendly. Then literally means after. We can also chain promises together. If any promise in the chain fails, the single catch block will handle the error. This is much cleaner than nesting callbacks. Sometimes, you might want to run multiple promises at once. For that, we have promise.all, which waits for all of them to finish and returns an array of results. There is also promise.race, which only returns the result of the promise that resolves the fastest. While promises are great, chaining many then blocks can still get messy. Programmers prefer writing code that looks synchronous even it's asynchronous. This is where async await comes in. By adding the async keyword to a function, it automatically returns a promise. Inside an async function, we can use the await keyword. Await tells JavaScript to wait for that specific asynchronous task to finish before moving to the next line. Even though it looks like it's waiting, it's not blocking the main thread. JavaScript skips the my meeting function call and continues with other code, returning to finish the function once the data is ready. We handle errors in async await using standard try catch blocks, which makes debugging very intuitive. To summarize, for the best performance, always aim for asynchronous non-blocking code. Use callbacks for simple tasks, promises to manage more complex flows, and async await to keep your code clean, readable, and organized. I hope this clarifies the concept of asynchronous JavaScript for you. If you have any questions, feel free to comment. In today's video, we're going to explore an advanced JavaScript concept called the memoization technique. In JavaScript, we often write functions that are very resource hungry or computationally exp- -ensive. When these functions are repeatedly called, they end up doing the same work over and over again, which wastes time and hurts performance. Imagine calling a function multiple times with the same parameters. Each time, it recalculates everything from scratch, even though the result is the same as before. Now, if that function is heavy, the performance will definitely take a hit, right? Memoization is used to solve exactly this problem. It's essentially a smart approach that stores the result of a function the first time it runs, so that the next time you provide the same input, it can return that result immediately without recalculating. So, in today's video, we'll see how memoization actually works, how to apply it, and which concepts we need to understand it properly. Let's get started without any delay. Let's start with a simple example. Imagine we have a function called add. This function takes a number as a parameter and returns that number plus 10. Pretty straightforward, right? But, let's assume this function is actually doing something very expensive or heavy. For instance, it might be fetching data from an API, performing complex calculations, or sending multiple requests. Basically, something that uses a lot of CPU and memory. Calling such a function repeatedly means repeating the same heavy work over and over, which is quite expensive for our system. This is the situation we are imagining. So, we are not actually writing a huge function right now, but let's assume our add function represents that CPU-heavy task. Now, when I call the add function with the parameter 10, it executes once. But, what happens if I call it again with the same parameter 10? Yes, the add function executes again. In other words, calling it twice with the same parameter means doing the same computation twice. The internal calculations happen two times, even though the input hasn't changed. Now, imagine if I keep calling it with the parameter 10 repeatedly, there's actually no need to recalculate every time, right? Because giving 10 will always return 20, but the calculation inside the add function still runs each time. So, if we want to do this heavy work just once and store the result in memory, so that next time the same input comes, we can directly use that result, that's what we call memoization. Let's look at an example of this. Simply put, when we call the add function, it performs its task, right? The add function is an independent function. Now, if we want it to remember previous results, we can't just call it directly. Instead, we pass the add function through another function. In other words, we don't call add directly. We send it to another function. This new function will handle it like this. I want to call this add function with the parameter 10 to perform a specific task. Check first if the result for this task is already stored in our memory. If the result is already cached in the function's memory, there's no need to calculate it again. It can simply return the previous result directly. But, if the result for the parameter 10 isn't in memory, it will call the add function, compute the result, and then save that result in memory. So, the next time someone calls it with the same parameter, it can return the cached result directly without recalculating. This is essentially the concept of memoization. I hope it's clear now. So far, we have understood the theory behind memoization. Now, let's see it in practice by implementing it. What we're going to do is create a memoized version of the add function. We start by writing a function like this. Function memo, and we will receive func as parameter. Here, the function is named memo. This memo function takes a parameter called func, meaning it accepts another function as input. That could be our add function. Inside the memo function, we will decide how to actually memoize the function, just like we discussed earlier. So, what does the memo function actually do? It takes a function as input and returns a new function. We can write it like this. Return function. This kind of function is called a higher-order function. That means it's a function that either takes another function as input or returns a function as output. So, to create a memoized version of any function, the first concept we need to understand is the higher-order function. A higher-order function is a function that can take another function as a parameter, returns a function, or do both. In our case, it's doing both. It takes a function as input and returns a function as output. Now, we will pass our add function through the memo function. This way, inside the memo function, we can decide, should we call the add function directly or first check if a previous result is already cached? So, the memo function itself becomes our memoized version, right? Now, look, we have already set up the entire boilerplate to create a memoized function for add. The next question is, how do we call this memo function? We can do it simply like this. Const calculate is equal to memo, and pass the add function body. Here, we are creating a new variable called calculate. Essentially, we are calling the memo function and passing the add function as a parameter. So, this calculate variable becomes our actual calculate function, which is really just a memoized version of the add function. See, the memo function is a higher-order function because we're not calling the add function directly. Instead, we are wrapping it inside another function, and that wrapped function is our memo. So, what is the memo actually doing? It takes the add function as a parameter and returns a new function. Inside this returned function, we decide whether to perform a new calculation or just return a previously cached result. Now, we will call the calculate function. We write console.log and pass the calculate function with parameter 10. The first time we called the add function, we wrote add 10. Now, we write calculate 10 in the same way. On the surface, it looks like the same operation, but internally, there's a difference. That's because calculate 10 is actually executing the function returned by memo. In other words, the calculate function is the function that memo returned. So, what does the memo function actually return? Yes, it returns another function. That means calculate is the function returned by the memo function. Now, since we are passing a parameter into calculate, we can receive it dynamically because it will go as input to the function returned by memo. So, let's say we captured it as a parameter called X. Now, think about calling the calculate function. First, we wrote calculate is equal to memo and pass the add function. Here, the memo function creates and returns a new function for the original add function. So, calculate is now a function that comes from memo. Then, we write calculate 10. Since calculate is the function returned by memo, the value 10 is passed as a parameter to that new function. In this way, we have essentially set up our boilerplate and it's working. Now, the next step is to decide how we handle caching or memory storage. These tasks will be done inside the new function returned by memo. Here, we need another concept. That's closure. Now look, you have created the calculate function just once. What does calculate actually do? It gives you a memoized version of the add function, right? In other words, when you pass any function, calculate creates its memoized version. So, the question is, what actually happens when you call memo add? The add function goes in as a parameter to the memo function, and memo returns a new function. Then, when we write calculate 10, we are using the same calculate instance each time, right? That is, calculate is the instance returned by memo. Now, all the work happens inside the return function. First, we need a cache. So, where should we put this cache? We put it inside the body of the memo function. For that, we write const cached is equal to a blank object. Here, we create an empty object named cached. You might wonder why we placed cached outside the inner function. That's because a closure is created inside the memo function, which allows us to retain this cache across multiple calls. So, calculate is the function returned by memo. No matter how many times we call calculate, it's actually pointing to the inner function inside memo. Whatever you do inside this inner function, the cache exists in its outer scope. That is, inside the memo function itself. As a result, a closure is created in the memo function, and every time we call calculate, the cache value is preserved in memory. In simple terms, we are returning a function from memo. Now, no matter how many times calculate is called, it always uses the reference to the inner function returned by memo. And the cached object in the outer scope of the inner function always retains its values. Inside the inner function, we decide whether to update the cache or return a previous value, right? So, I hope this is clear now. The first time we call calculate with 10, what happens? We write calculate 10. Since calculate is the inner function returned by memo, that inner function executes with the parameter 10. First, it checks if cached 10. Since there's nothing in the cache yet, it doesn't enter the if block. Instead, it goes to the else block and we print to the console console.log calculating result. Now, to get the actual result inside the else block, we call the original function. That is func, which we passed as a parameter to the memo, is called with the parameter 10. So, we write const result is equal to func and pass the X. Here, func is the original function and X is the parameter. This way, we calculate the result for the first time and then we can store it in the cache. Now, we have the result from the add or func function stored in result. After getting the result, we update the cache by writing cache X is equal to result. This means we have stored the result in the cache under the parameter X. Our cache is now set up. Finally, the inner function needs to return a value. So, we return return result. This completes the process for the first time. That is, for the else block. The second time we call calculate with 10, the condition if cached 10 will match. That's because last time in the else block, we updated the cache. And since the cache exists in the outer scope and is preserved by the closure, it remembers the value, allowing us to return the result directly from cache 10. So, now inside the if condition, we can simply write return cache X. This creates our memoized version. If we look at the output, the first time we call calculate, the console will show calculating result. That means it entered the else block, didn't use the cache, called the func function to get the result, and then stored that result in the cache. But the second time we call the same calculate 10, it enters the if block and the console shows result from cache. That means it doesn't recalculate. It directly takes the result from the cache. So, I hope the concept of memoization is crystal clear now. We essentially used two concepts. One is the higher-order function. The memo function itself is that higher-order function. What did we do? If we call the add function directly, it would execute repeatedly. So, instead we pass the add function to another function. That function decides whether to take the result from the cache or calculate it anew. And the function that can make this decision is the memo function. Now, the point is memo is a higher-order function. We stored the memo function in a variable called calculate. And later we call the inner function returned by memo using calculate. In other words, calculate is actually the function returned by memo. This inner function is ultimately what decides whether the data exists in the cache, whether to store the new data, or whether to return the result directly from the cache. When we call calculate 10 for the first time, the inner function executes. At that moment, you will see there's no data in the cache for the parameter 10. So, it goes into the else block, calculates the result, updates the cache, and returns it. The next time we call it with the same parameter 10, the cache is already updated. To visualize this, we can write inside the inner function console.log cache. This will show the current state of the cache. The first time the cache is empty. The second time it shows 20 for the parameter 10. In other words, we are using the parameter as a unique key. Next time, if we pass 10 again, the inner function will return 20 directly from the cache. If we pass a different parameter, it will calculate a new result and update the cache. I hope this makes the concept clear. Now, one last point. It's not always the case that there's only one parameter. The number of parameters can be anything. For example, our add function might have unlimited arguments, and we want to calculate the sum of all of them. So, we won't just call calculate 10. For instance, we can write console.log calculate 10 20 30 40. Here, we are passing multiple arguments. In that case, we need to create a dynamic function that can handle all parameters and sum them up. So, we write the add function like this. Function add, and we will use the rest operator, then x. Here, we are using the rest operator to capture all parameters and naming it x. That way, when the add function [clears throat] is called, all arguments are collected into an array called x. To see this in action, instead of calling calculate, we will call add directly. First, inside add, we write console.log x. Then, we call the add function. Console.log add 10 20 30 40. Looking at the output, we can see what x actually is. It will be an array of parameters. In other words, when you use the rest operator inside add, all parameters are collected into the x array. So, x simply holds all the parameters through the rest operator. Now, returning to the implementation of add, we need to sum all the parameters. Since x is an array, the easiest way to sum all its elements is to use the reduce method. So, we write return x.reduce, and that's how reduce operates. We will get the sum and current value in the first parameter, and then we'll return sum plus current value. And in the second parameter, we'll pass zero as a default value for the first time. We know reduce takes a callback function. This callback function receives the cumulative value, which is the sum so far, and the current value, car val. We also provide an initial value of zero. So, sum plus car value gives the total sum of all parameters passed to the add function. This is now the implementation of the add function. But, keep in mind, this is just one way to implement it. Inside the memo function, nothing has really changed. However, the inner function that memo returns will now have more than one parameter. What should the inner function's parameter be? Again, we use the rest operator. Return function rest x. Since we will receive multiple parameters, the rest operator captures all of them. Now, creating the cache is a bit different. X is now an array. When we call calculate 10 20 30 40, we need to turn that array into a key, like a string. This is because the result for the combination 10 20 30 40 is different from 50 60 70 80. In other words, the combination of parameters need a unique key. In earlier case, with only one parameter, we could directly use x as the key. But now, with multiple parameters, we need to convert the x array into a string or some other unique format to use as the key. So, before checking the cache, we create a constant key. Const key is equal to JSON.stringify, and I will pass the x parameter. Here, we use JSON.stringify because x is an array. Stringifying it gives us a unique identifier. We can also check what the key looks like. Console.log key. Then we check the cache like this. If cached key, that is, we check the cache based on the key. If the key already exists in the cache, we return cached key directly. If not, we calculate the result. To calculate, we write const result is equal to func, and I will use the rest operator, then x. Here, we use the spread operator because func is our add function. The add function expects each parameter separately. If we pass the X array directly, it won't work. Using the spread operator sends all elements of X as separate parameters. Then, we update the cache. Cached key is equal to result. That is, we save the result under the key in the cache and finally return result. Now, if we check the output, we will see the sum of all values. The first array printed came from the inner function's console.log. But, what we want is to print the cache instead. console.log cached. Initially, the cache is empty. On the first call, calculate computes the result and stores it in the cache. Now, if we call it again with the same combination of parameters, console.log calculate 10 20 30 40, you will see that the first time the cache was empty. The next time the cache has a key created for this combination, and the result is stored under that key. We used stringify so that the key becomes a string. This is how the whole process works. So, I hope it's clear now. This concept is commonly asked in job interviews and is frequently used in JavaScript for performance optimization. If you work with React, you are probably familiar with the useMemo hook, which essentially works in the same way as a higher-order function to achieve the same goal. So, what once seemed difficult about memoization should now feel much easier. You'll be able to explain clearly in a job interview what memoization is and how it works. If you enjoyed this video, don't forget to hit the like button. Drop a comment to let me know your thoughts, and share it with your friends so they can get benefited too. Thanks for watching, and I will see you in the next one. We all know that JavaScript is a single-threaded programming language. But, what does that actually mean? It means that, no matter how many processor cores your computer or machine has, JavaScript will only use one core to run its tasks. The rest of the cores essentially remain idle. Sounds a bit scary, right? Having so many cores yet still running everything on just one. And that's exactly why JavaScript is called a single-threaded programming language. Now, the question is, how does JavaScript handle this limitation? The answer is through the event loop, a non-blocking event loop. I already have a detailed video in Bangla explaining the event loop, and you'll find the link in the description if you want to check it out. I will soon publish the English version of it, but here I will give you a short explanation. The most important thing about is that it's non-blocking. For example, imagine sending a request to an API URL. When you hit that URL, the entire program doesn't freeze or get stuck at that point. Instead, JavaScript sends that request off as a network task and immediately moves on to the next line of code. In other words, no block is created in the main thread. The task continues in the background elsewhere, while JavaScript keeps executing the next instruction in the code. After the network request is sent, the moment a response comes back, the callback function inside the previously written then block gets executed. However, this callback function still runs within the main thread. That means during the request or while there was latency, the rest of the code didn't get stuck. It kept executing normally. This is exactly how JavaScript manages everything even while using a single thread. It doesn't just sit idle. Instead, any task that can be offloaded is sent to the background, while it keeps moving forward to execute the next instructions. The tasks that must run in the main thread happen right there, and the ones that can be handled separately are completed elsewhere and then return to the main thread through callback functions. This entire process is what we call concurrency. Concurrency means handling multiple tasks at the same time. To put it simply, imagine a restaurant with only one chef. Now, the chef is getting many orders at once. Cooking is his main job. That's like the main thread's work. But apart from cooking, there are other tasks, too. Like chopping vegetables, grinding spices, or other prep work. Instead of doing all those himself, the chef delegates them to others. We can think of these helpers as being like network requests. They are not handled by the main thread, or in our example, by the chef, but instead happens separately on the side. Once the task is done, they let the chef know the vegetables are chopped or the spices are ready. At that point, the chef takes those prepared items and uses them in cooking, or moves forward to the next step in the process. In other words, the chef is only doing one thing at a time, cooking, but the people around him are doing different tasks to support him. As a result, the chef doesn't have to sit idle and wait. He can keep focusing on his main job. JavaScript works the same way. By using concurrency, it manages multiple tasks at once. But there's the interesting part. You can't handle everything with just concurrency. This is where another important concept comes in, and that is parallelism. So, the question is, what exactly is parallelism? In the restaurant example I gave, imagine if instead of one chef, there were five chefs working together. Just like your computer processor has five, eight, or even more cores, with five chefs, the restaurant could definitely handle a lot more orders at the same time and serve them much faster. At first glance, it might seem like the solution is simple. Just add more chefs. But in reality, it's not that simple. Parallelism means running multiple threads at the same time. Programming languages that use this approach are called multi-threaded languages. For example, PHP, C#, C, Go, Java, and many other rely on multi-threaded architecture. At first glance, it might seem like these languages should perform much better than a single-threaded language like JavaScript, since they can utilize all the threads. But the truth is, that's not always the case. In every situation, using a multi-threaded language doesn't automatically guarantee better performance. Let me explain the reason behind this. Let's go back to the earlier fetch API example. Fetching is actually an IO-intensive task. IO stands for input-output. So, what exactly are IO-intensive tasks? Things like reading and writing to the file system, fetching or saving data from database, or handling network communication. All of these are considered IO-related operations. These kind of tasks are mostly carried out externally. For example, when you read or write to the file system, the response comes from the disk. The same goes for the database. You have to wait for the database to respond. And in the case of network communication, you wait until the server sends back a response. None of these tasks happen directly in the main thread. They're all IO-intensive tasks. Now, here's the important part. These tasks are not CPU-intensive. During this time, the CPU is practically unused or idle. But in many multi-threaded programming languages, even for IO-intensive tasks, CPU threads get blocked. Yet, if the CPU could be freed up during these operations, tasks like handling network requests or performing disk read-writes could be completed much faster. And this is where JavaScript truly stands out. For IO-intensive tasks, JavaScript is highly efficient because it manages these operations without blocking the CPU. That's why JavaScript is so widely used in areas like real-world communications or network communications. And this is the real explanation of IO-intensive tasks. Now, let's move on to CPU-intensive tasks. These are the kinds of operations that must be done on the main thread because they directly use the CPU. For example, running a very large loop, iterating through a huge array with a for loop, or doing image processing or video processing. All of these tasks require CPU's power to execute. Now, if we take JavaScript as an example, since it runs on a single thread, these CPU-intensive tasks must be handled on the main thread. Unlike IO-related tasks such as fetch, these can't be handled off to someone else. It's not like the chef delegating the chopping of vegetables. Here, the chef himself has to do the actual cooking. That's why until these tasks are finished, the main thread actually stays blocked. For example, in the for loop you see on the screen, the code below it can't run until that loop is fully completed. But in the fetch example we saw earlier, execution was able to move on to the next line right away. So, the question is how does JavaScript handle situations like this? The answer is yes, JavaScript does have a solution for it. We know that JavaScript is single-threaded, but there's the surprising part. If needed, JavaScript can actually be used in a multi-threaded way, and that's the beauty of JavaScript. Today, that's exactly what we're going to talk about. This is made possible through the Web Worker API. It's a browser API that allows us to create new workers, or in other words, new threads from our script. Once created, we can offload certain tasks from the main thread to these new threads. Now, let's look at an example. Once you see it, you will understand just how amazing this really is. So, let's jump straight into the demo. We will start with a very simple example. As you can see, in our VS Code editor, we have an index.html file open. Inside it, there are two buttons, each with a unique ID. One is total, and the other is PG. We have also included a script file. This is just a very basic HTML setup. If we run this file in the browser, we will see two buttons on the output. One is the calculate total button, and the other is the change background color button. Now, what happens when someone clicks the change background color button? That logic is written inside our script.js file. For the PG button, we have added a click event listener using plain vanilla JavaScript. The logic is simple. If the background color of the document's body isn't green, it will turn it green. And if it's already green, then it will switch to blue. So, basically, it's working like a switch or toggle. If we go to the browser and click the change background color button, the background will first turn green, since it wasn't green before. Clicking again will change it to blue. And with each click, the color will keep toggling back and forth. I have only added as a simple demo so that it becomes easier to explain the main concept later on. The real part lies in the logic for the calculate total button. As soon as you click this button, the main action begins. If you look closely at the code, you will notice there's a massive for loop written there. A loop that runs up to a number with 10 zeros after one, meaning it iterates a very large count. Inside this loop, we have declared a variable called total. And in each iteration, we add one to its current Since the loop runs up to such a huge number, this operation takes quite a bit of time. And finally, once the loop is done, the value of total gets printed in the console. As I mentioned earlier, this task you see here is a synchronous operation. In other words, it's completely CPU intensive and has to be executed on the main thread. This isn't something that can be handed off to someone else. JavaScript itself must handle it within its own thread. That's why until this task is fully completed, the code simply cannot move on to the next line. Now, if we see this in action in the browser, say we reload the page and then click the calculate total button, you will notice nothing happens right away. No output shows up in the console. Notice another thing. If I click the change background color button during this time, the background color doesn't change either. That clearly means the main thread is completely blocked. While this huge loop is running, no other task can be performed, right? Even this line here hasn't executed yet. So, until the for loop finishes its work, no further code will run and no other action can take place. Now notice, as soon as the for loop finishes in the browser, the background color changes instantly and we can also see the output in the console, right? At this point, everything is back to working normally. But, if you click the calculate button again, the same thing will happen. Once again, the main thread will be blocked and during that time, you won't be able to do anything else because JavaScript runs this loop directly on the main thread and while the loop is running, the main thread remains blocked, leaving no room for any other task to be executed. So, this is the real problem. Here, I only showed a massive for loop as an example, but in real-world scenarios, you might face many similar situations like processing a large array, running complex image processing, or handling any other heavy operation. These kinds of tasks are CPU-intensive and take a significant amount of time. But, think about it. If this task could be done in the background and once completed, we could take the necessary action, then the main thread wouldn't get blocked at all. You'd still be able to continue doing other tasks without any interruption. And this is exactly the benefit of using the Web Worker API. With it, we can take CPU-intensive heavy tasks out of the main thread and run them in the background instead. In this case, our very first step will be to create a separate thread, meaning we will make a new worker. We will then use that worker specifically for the heavy tasks without mixing it up with the main thread. By doing this, the heavy tasks will be handled in a separate thread while the main thread stays free to continue other work. A webpage can have multiple workers if needed, but since we only have one heavy task here, we will just create a single worker for now. So, at the very beginning of our script.js file, we will write const worker new worker. All right. Here, new worker is a class, and worker is simply an instance of that class. We have now initialized a worker. The worker class expects the location of your worker script file as its parameter. In other words, whichever script file path you provided will be executed in a separate thread outside of the main thread. Right now, we don't have a separate script file. So, we will create a new one and name it worker.js. Our worker.js file is now created. We haven't written any code inside it yet, but before that, let's pass the script path into the worker class parameter. Since the worker.js file is in the root folder, we can simply provide worker.js as the parameter in the worker class. Now, inside the worker.js file, we will write our code. This is where we will put the CPU-intensive task or heavy operation. But, you might wonder, can we just copy the for loop from script.js directly into this file? The answer is no. That won't work because worker.js runs in a completely separate thread, and as soon as you used new worker, JavaScript instantly created a new thread and isolated it entirely from the main thread. Because script.js runs on one thread, and worker.js runs on another. So, there has to be a way for them to communicate. Since they're working in completely separate spaces, you can't just directly import and use one inside the other. What we need here is messaging. Think of it like this. You are in Canada, and your friend is in Africa. You can't see each other directly, but you can send messages. And those messages reach the other person. The same idea applies here as well. From the main thread, you can send messages to the worker. For example, something like "Hello, brother." And once the worker finishes its work, it can also send a message back to the main thread. Whenever you initialize a worker in script.js file, that worker immediately knows it can send messages to the main thread at any time. Similarly, the main thread can also send messages to any of its workers. So, there's a communication channel. This is essentially how the communication between the two takes place. For example, let's say from the main thread, script.js, we can send a message to the worker thread. That means the worker.js. The message would be something like, "Brother, I need to run a really a big loop. Can you handle it for me?" The worker thread will then receive that message, run the loop, process the data, and once it's done, it will send the processed data back to the main thread. But before you look at the example, the first thing we need to do is cut the big for loop out of script.js and move it to worker.js. So, let's cut the loop from script.js file and keep it in the clipboard for now. We won't paste it into the worker.js just yet. We will do that in a moment. Now, inside the event listener, we will write some logic. The idea is simple. When the user clicks the total button, we will send a message to the worker. How do we send it? Right where the for loop used to be, we will write worker. P. You will notice that VS Code immediately shows an auto suggestion. Among the options, you will see post message. This means that inside the worker object or instance, there's a property called post message. And the The itself is very intuitive. Post message simply means we are posting a message to the worker. Now, if we write worker.postMessage, "Hello worker." That message will be sent from the main thread to the worker thread. But for the worker to actually receive this message, we need to set up a listener inside the worker.js file. It works just like an add event listener. So, whenever a message comes in, the worker will be able to catch and process it. Exactly that's how we it inside the worker.js file. Here, we can write self.onmessage. This means the worker is setting up its own listener. Same like we use document.addEventListener. So, whenever a message arrives from the main thread, it will listen for it right here. To understand this better, compare it with script.js. Just like the global window object exists there, in the case of a worker, self acts as its global object. The difference, however, is that a worker thread cannot access the browser's DOM. That means it has no access to the window object. And you also can perform any DOM operations inside it. However, you can perform any kind of CPU-related work here. Just remember, there's no DOM access in this context. That means methods like document.getElementById or anything similar can't be used inside a worker. Just like when running JavaScript in the browser, we don't always have to write window.setTimeout or window.document because window works in the background by default. In the same way, inside a worker.js file, writing self.onmessage is actually optional. You can simply write on message directly or use self.onmessage. Both will work. It's completely up to you. So, in the worker.js file, we will assign an anonymous function to on message. Just like we usually write a callback function in case of document.addEventListener. In that function's parameter, we will receive a message object. This is the exact message that comes from script.js where we used postMessage to send it. For now, just to test it simply, we will write a console.log message inside the function body. That way, whenever the worker receives a message from the main thread, we will be able to see it printed in the console. All right. Let's recap what just happened. From the addEventListener of the total button in the script.js file, you will send a message to the worker thread using the postMessage function. And in worker.js, whenever the onmessage listener receives that message, it will immediately print the message in the console. Now, let's test it in the browser. After reloading the page, click the calculate total button. You will see that a message object has been printed in the console from worker.js. If you expand that object, you will notice it has a data property. And inside it, the value is hello worker. That means the exact message we sent inside the total button's event listener in the script.js file with postMessage hello worker was received by worker.js and it logged that message to the console, which is exactly what we are seeing in the browser. Now, we will move on to the real task, that CPU-intensive operation, the massive for loop we had cut earlier and kept in the clipboard. We will now paste it into the worker.js file. From this point on, running that loop becomes the worker's responsibility. Now, the mechanism will work like this. Whenever you send a message to the worker from the total button event listener, the onmessage function inside worker.js will be triggered. And right there, the worker will start running the big loop. Let's say, in script.js, you wrote worker.postMessage "Hello worker, please do the work for me." Now, worker.js will receive that message. Earlier, we were logging message.data to the console, but this time instead we'll print "Worker has started working." Then the worker will run the loop, and once the loop is finished, it will print "Worker has finished working." This way, worker.js will clearly notify us when the work has started and when it has ended. So now, let's check the demo. I went to the browser and clicked the calculate total button. Notice what happened. Right away, the console printed "Worker has started working." That means the worker has begun running the loop in the background. During this time, I clicked the change background color button. And the background color changed perfectly. That's because the main thread isn't blocked anymore. The worker is handling its task in its own thread, while the main thread is free to take care of other actions. A little while later, the console showed "Worker has finished working." That means the worker completed its task and notified us. So, we clearly saw both when the work started and when it finished. And at the same time, our main thread stayed fully active. We were able to smoothly change the page's background color without any interruption. But here's the question. The worker has finished its job, but the output is actually needed in the main thread. In real scenarios, you might have the worker perform some heavy calculation, but then you will need that result to display in the DOM or use in the main thread. So, how do you send a message back from the worker thread to the main thread? It's very simple. After the worker finishes its task, it just needs to send a message back to the main thread. In worker.js, once the loop is done, you can use post message to send the result to the main thread. And inside that message, you can send anything you want. In our case, we will send the calculated total value. So, inside post message, we will pass the total variable. But how will the main thread receive this message? For that, we will also need a listener in script.js, right? Here's what we'll do. Since we already have the worker's instance or object, we will call an onmessage method on it. Notice, we will write worker.onmessage. Remember, in the worker.js file, we just wrote onmessage to receive messages. The same way, here in the main thread, we will use worker.onmessage to catch messages coming from the worker. As the value of onmessage, I wrote a function. This function takes a message parameter, which is actually an object. Inside that object, the calculated sum will be stored in the message.data property. So, once the worker thread finishes its task, I will print it with console.log like this. The total is message.data. I hope by now you clearly understand the entire flow. So, let's see it in the browser. I reloaded the page, then click the calculate total button. In the console, I can see the log showing that the worker has started its task. At the same time, when I click the change background button, the background color changes as well. As you can see, everything in the browser is running smoothly without any issues. The worker is doing its job in the background, while the main thread is handling other tasks properly. Look at that. The console shows the message, worker has finished working. And finally, right below it, the total value appears. But, remember, this output is actually being shown from the main thread. The console log came directly inside the main thread. I hope you can see how beautifully this works. By using workers, we can handle large, heavy tasks in a very smart and efficient way. What I just show you is the Web Worker API in the browser. Similarly, as you know, JavaScript doesn't only run in the browser. It also works on the server side. Since Node.js is a runtime, it also has a worker API. And you can use workers there as well. The implementation is a bit different, but the core logic and concept remain the same. Hello everyone. I hope everyone is doing well. In today's video, I will talk about how to implement multi-threading in Node.js. As we all know, JavaScript is a single-threaded programming language, and Node.js is the runtime environment for JavaScript. This means that JavaScript essentially runs within Node.js, and all operations are handled through a single thread. However, when we perform tasks that require heavy processing, Node.js performance can start to decline. Many people mistakenly think that Node.js is not good, or that JavaScript is flawed. But, there is actually a solution. JavaScript can also be used effectively with multi-threading. Earlier, I showed in a video how we can implement multi-threading in the browser using JavaScript. In today's video, however, we'll focus on the back end. Especially, how to implement multi-threading on the server side using the Node.js runtime. As always, we will start today's discussion with a simple example. Imagine you have a basic Express.js application in front of you. Especially, a Node.js app. But, let me clarify from the start. This tutorial is not meant for absolute beginners. It's primarily an advanced-level tutorial. When we talk about worker threads or multi-threading, the concepts naturally become a bit complex and high-level. So, before diving into this, you need to have a clear understanding of the basics. Only then can you progress to this level. So, let's get started. Here, you have a simple Express.js application. I have required Express from the Express module and then created an Express app in the usual way. After that, two routes have been added to this application. One is {slash} non-blocking and the other is {slash} blocking. The {slash} non-blocking route is straightforward. It simply returns a text response saying, "This page is non-blocking." On the other hand, the {slash} blocking route contains a large loop that runs up to 1 million numbers, calculating the sum of all these numbers and then returning the result. Finally, the application is set to run on port 3000 using app.listen. I'm running this application with Nodemon, so our server is now up and running, showing the message, "Server is running." Now, if you go to the browser and hit the {slash} non-blocking URL, you'll see it works perfectly fine. But, if you hit the {slash} blocking URL, you'll notice that it keeps loading and doesn't respond immediately. What's even more interesting is that if you try to access {slash} non-blocking again during this time, it also gets stuck and doesn't respond. What does this mean? It means that while the {slash} blocking route was executing, even the {slash} non-blocking route became unresponsive. Why does this happen? The reason is that Node.js is essentially a JavaScript runtime. We all know that JavaScript is a single-threaded programming language. Naturally, Node.js also runs on a single thread by default. So, where does the problem arise? When you execute the {slash} blocking route, the JavaScript code runs on the main thread. During that time, the main thread is completely busy or blocked. As a result, if another user tries to access the {slash} non-blocking route, they won't get any response because the main thread is still occupied with the previous task. This is why many people think that JavaScript is weak because it's single-threaded. But, the truth is this perception is not entirely accurate. With the right approach, JavaScript can also be used in a multi-threaded way. Let's imagine this is our main thread. Now, a question might arise, where exactly does JavaScript execute? JavaScript runs inside the JavaScript engine, which is responsible for converting JavaScript code into machine code. Node.js runs on the V8 engine, the same engine used in Google Chrome. The V8 engine operates entirely on a single thread, meaning everything executes within just one main thread. Now, the question arises, are there any threads other than the main thread? The answer is yes. Simply put, besides the main thread, there are additional threads. These extra threads are used to handle different types of tasks. The management and implementation of these threads are handled by a special library called libuv. Now, the question is, what exactly is this? It's essentially implemented to work alongside your V8 engine. While the V8 engine executes code on the main thread, there are additional threads used for different types of tasks. For example, operations like database queries, network requests, or file read-write tasks are handled by these extra threads, and the libuv library manages and coordinates them. Whenever we perform such tasks, these are actually handled by these extra threads outside the main thread. libuv instructs the V8 engine on how to execute these tasks. These kinds of tasks are called input-output operations, or IO operations for short. In other words, when performing file read-write, database queries, or network requests, these IO operations are executed on separate threads without blocking the main thread. However, if we have something like a for loop in our example, or any task that primarily requires CPU processing, it doesn't fall under IO operations. In such cases, the tasks has to be executed on the main thread, leaving no option but for the main thread to become blocked. Let's say a client sends a request to the main thread. For this request, a query needs to be made to the database eventually. So, when a user sends a request that requires a database query to be served, the request is sent to the database, but it doesn't block the main thread. Instead, libuv query on a separate thread, keeping the main thread free. In this situation, if another user sends a request that doesn't require any database query or IO operation, it will execute quickly on the main thread. As a result, this user can receive a response immediately. When the database query running on the separate thread completes, the result returns to the main thread, which then sends it as a response. This way, users receive their output, and since the main thread remains free, other users can continue their tasks without delay. So, this entire process represents the asynchronous nature of JavaScript or Node.js. In other words, tasks here are not executed synchronously, but asynchronously. One user's request is being processed on a separate thread, and when another user comes along, they find the main thread free and can use it immediately. This is how the asynchronous behavior of JavaScript or Node.js works. So, this is how everything works effectively. Now, the question is, what happens if the main thread has a task that doesn't require any database access for a user's request, but demands heavy CPU processing? In that case, the main thread will get blocked. Let's say a task on the main thread is consuming a lot of CPU. If we execute it directly on the main thread, the event loop will get blocked, and other requests won't be able to be processed. This is where worker threads come into play in Node.js. With worker threads, we can spin up a new thread outside the main thread to handle CPU heavy operations separately. As a result, the main thread stays free, allowing other requests to be processed immediately. In other words, by using worker threads, we can run CPU bound tasks asynchronously, ensuring that the server's throughput and responsiveness are not affected. So, looking back at our code example, you will see that the task in the {slash} blocking route handler is entirely running on the main thread, which is why this part is blocked. Now, how do we solve this? For the solution, we can use Node.js built-in worker threads module. There's no need to install any separate package to use worker threads, as it's a core module of Node.js. We can directly require the worker from the worker {underscore} threads module, and from there create a new worker thread. We have required the worker, and now it's time to use it. First, in the {slash} blocking route handler, we need to create a new worker. When creating a worker, we use new worker and provide a file path. This path points to the JS file that contains the task we want the worker to perform. Our heavy operations, like the for loop, are placed inside that file. So, I have moved this for loop into a separate file named worker.js. For now, the for loop is pasted there. Then, in the {slash} blocking route handler of index.js, we provide the path to this file when creating the worker. Doing this initiates a new thread, which will handle our CPU heavy or CPU hungry task. Let's go back to our slide. Here, we have the main thread, and the other threads are on the worker threads. There's a messaging system in place for communication between them. In other words, everything ultimately has to reach to the main thread. Otherwise, we won't be able to provide any output. Let's say you assign a task to thread B and another to thread C. When thread B and thread C complete their tasks, they need to inform the main thread. To do this, they use the messaging system to send messages. For example, thread C will send a message directly to the main thread. It works just like exchanging messages in an inbox. Through this messaging, worker threads notify the main thread that their tasks are complete and also send the necessary data. This is exactly the process we will use here. So, we have created a worker.js file. Now, the question is, how do we inform the main thread about the task being done in this file? For this, we extract parent port from worker_threads module. The parent port is an object that allows the worker thread to communicate with the main thread. Once the task is complete, we use parentPort.postMessage and in the bracket, pass the result. In other words, we send a message and in our case, that message is the result data. Now, the question is, once I send the data, how do you receive it in the /blocking handler of index.js? This means we need to set up a listener inside the handler. For this, we use worker.on. What do we listen for? We listen for the message event. Just like we listen to onclick or other events in JavaScript. The first parameter is message and the second parameter is a callback function where the first parameter receives the data. Then, we can send this data as a response using res.status in the bracket 2000, that means the status code, dot send and in the bracket, result is data. At the same time, if any error occurs, we can also listen for it using worker.on. What do we listen for? We listen to the error event. Just like before, the first parameter is this time error and the second is a callback function where the first parameter receives the error message. Then, we can show the error using res.status, in the bracket, the error status code 400. .send, and in the bracket, we can pass the error message that an error occurred, and pass the error message. Once this is set up, you will see a dramatic change. Let's go to the browser and check the {slash} blocking route. It's loading, but even while it's loading, repeatedly refreshing the {slash} non-blocking route works perfectly without any issues. Now notice, the {slash} non-blocking route is accessible, which means even though the {slash} blocking route is still running, it doesn't affect anything. So, we have successfully solved this problem. We moved the main task to a separate thread outside the main thread. What does this mean? The main thread created a new worker thread and assigned the CPU heavy task to it. The new thread now works independently while the main thread remains free. Finally, when the new thread completes its task, it also becomes free. Then, through the messaging system, the new thread informs the main thread, "Your data is ready. Here's your data." The main thread receives this data and sends it to the client as a response. Therefore, the tasks that were automatically handled on separate threads for database queries or file read-write operations, because they were IO operations, we have now manually initiated a thread and used it to handle similar CPU heavy tasks. So, I hope you now have a clear understanding of how this works. Next, we will optimize this process further and move on to a slightly more advanced level. Look, when I go to the {slash} blocking route, it's still taking a lot of time. This means the optimization isn't fully complete yet. We have used a separate thread, meaning we have utilized one CPU core outside the main thread. But, our machine has many more cores. If I open the terminal and want to check how many cores my machine has, I can run the command sysctl /n hw.nCPU. This is a Unix command. Running it will show how many cores are available. For example, I have 10 cores since I'm on a Mac. If you are using Linux instead of Mac, you have used the nproc command. And on Windows, you can use echo number of processors to check. So, out of the cores available on your machine, you can choose to use one or multiple cores. I have 10 cores here, so I have decided to use four cores for this task. In other words, you can decide how many cores to allocate for a particular job. Now you use multiple threads instead of a single thread, the task will be completed much faster. So, now we will optimize this task. First, we create a copy of index.js and name it index_optimized.js. Then copy all the code from index.js into it. Similarly, we make a copy of worker.js, name it worker_optimized.js, and copy all the code from worker.js. Now, coming to the main point. We'll be using threads, right? We have planned to use multiple threads. Let's say we have decided to use four threads. Our machine has 10 cores, but we won't use them all because that would consume all our system resources. So, we will use four threads from four of the available cores. For this reason, in the index_optimized.js file, I have created a constant to store the number of threads we will use. Let's say I have set it to four here, so that later a programmer can easily change it if needed. Then, I have created a new function called create worker. The purpose of this function is to create a new worker. Here, I'm returning a promise because the process of creating a worker is performed asynchronously. This is because when we create four workers, we want the creation process itself to happen asynchronously. So, the main thread doesn't get blocked. After all, creating a worker is essentially a separate process. The best practice is to create workers asynchronously. That's why I created the create worker function, which returns a promise. As we know, events are listened to inside a promise, where resolve and reject are used. In the {slash} blocking handler, we can handle the worker's result or any errors through this promise. To create a worker, we use const worker new worker, and here we need to provide the path to the worker file. Then, as the second parameter, we can pass some options. For example, if we want to send some data to the worker, we use worker data. Inside this worker data, we will send a thread count, which is stored in our file as thread_count. For instance, we can pass an object in worker data like thread count: thread_count. When this worker is being created, we send some properties from index_optimized.js as worker data. This is because in worker_optimized.js, the worker can use parent port to know how many threads it should use. So, we have included a thread count property in worker data. When the worker starts, it reads thread count from worker data and works accordingly. This is how we have designed the create worker function, which simply returns a promise. Here, I made another change. I removed the worker variable from the {slash} blocking handler, because we are now creating the worker inside the create worker function. The event handlers from the {slash} blocking handler have also been moved into the create worker function. The worker is created inside the function, and the listeners are set up under the worker there. However, since these events are being listened to inside a promise, we cannot send a response directly from there. We will send a response inside the slash blocking handler. So, from the promise, we only use resolve and reject. For example, if we get a success message, we call resolve and in the bracket, result is data. And if there's an error, we call reject an error occurred and pass the error. In other words, the entire process of creating a worker has been moved into the create worker function, which ultimately returns a promise. The worker data we provide tells the worker what task it should perform. The created worker is linked with parent port in the worker optimized.js file, which essentially communicates with the parent thread. Now, we want to divide the for loop running up to 1 million across four cores. The number of cores to use is sent from the index.optimized.js file as part of worker data. Because this information is in worker data, the workers can automatically divide and handle the tasks among themselves. So, in the worker optimized.js file, I will get the worker data using worker data. Here, we are destructuring the worker data from the worker threads object. Then, in the for loop condition, I will use worker data.thread count. This means the thread count sent from index.optimized.js will be used here instead of hardcoding the number four. This is best practice because the data is passed to the worker at the time of its creation. In worker optimized.js, we use this to divide the work into four parts. Then, four workers will be created, meaning the create worker function will be called four times. Each worker will take one part of the work and at the end, all results will be combined. This is how the entire process is completed. So, in this blocking handler, our task is to collect the results of the four promises and then sum them all. Let's say we stored them in an array called worker promises. Each entry in this array will hold the promise result of a worker. Then, by combining all of them, we get the final result. Since I need to create four workers, I will run a for loop. For let I is equal to zero, I less than thread underscore count, I plus plus. Inside the body of this loop, I will call the create worker function each time. This means that in every iteration, a new worker is created and its promise is pushed into the worker promises array. So, inside the body of this loop, I will call the create worker function four times. Each call to create worker returns a promise. These four promises are pushed into the worker promises array like worker promises.push. In the bracket, I will call the create worker function. This way, each worker has its own promise. In the end, since all the promises are stored in the worker promises array, we can easily call the promise.all and pass the worker promises. So, I used thread results is equal to await promise.all in the bracket worker promises. As we know, promise.all can handle multiple promises together. Here, we pass the worker promises array, so thread results will contain the results of the four promises as separate elements like thread results zero, thread results one, thread results two, and thread results three. Then, we sum these results to get the total calculation, meaning thread results zero plus one plus two plus three gives the final result. Since I used await, the entire function needs to be async. Once everything is done correctly, I can send this total result to the client using res.status, pass the 200 success status code, dot send, and in the bracket we can pass the success message. Result is total. This way, the total calculation works correctly, unlike before. So, I hope it's clear now. I call the create worker function four times here. Each call returns a promise. I then awaited all these promises together using promise.all, so all the results came at once. After that, I summed these results. The {slash} blocking handler is essentially the one executing our operational work. So, in the worker_optimized.js file, we have essentially divided the work into four parts. But, it's not necessarily that the task will always be a for loop. There could be different types of complex tasks as well, like image processing, data processing, or pagination. In such cases, we can't always follow the same pattern. So, we need to send the necessary data from index_optimized.js as worker data, and the worker will use that data to perform the task in a separate process. In the previous example, all the steps were sequential. So, simply summing the results gave us the total. But, in the case of complex tasks, we need to use data-driven processing. In other complex applications, you might need to perform different tasks. But, the main concept is clear. Any data or property we send from here will be received by the worker, which will then divide the work. Each worker, whether you use four, five, or six, will handle its part, and all the results will need to be accumulated. This is essentially the entire process. So, now if I run the index.js file and hit the {slash} blocking route in the browser, you will notice that it's taking quite a bit of time. So, now I run the index_optimized.js file. If I hit the {slash} blocking route in the browser, you will see it takes comparatively less time, around 3 seconds. Then, if I stop it and run the same route with index.js again, it takes much longer. This shows that by dividing the work into four parts, the process becomes much easier. Clearly demonstrates how worker threads can make our tasks far more efficient. So, first we saw in index.js how a blocking task can be handled in a non-blocking asynchronous way. That is, we ran a worker thread and because of this worker thread, the main thread didn't get blocked, allowing other users to continue their tasks simultaneously. But, the problem is when you use a new thread on the server, there isn't just a single core. Usually, there are multiple cores, like eight, 16, or more. To use multiple cores, we first need to find out how many cores are available on the server. If the server is a Linux server, we can easily find out the total number of cores using the l nproc command. Then, we can decide how many cores to use. For example, let's say we decide to use three cores. In index-optimized purchase, we have implemented a way to divide the work among these cores. So, what we did was wrap the worker creation process in a promise. Since creating a worker takes some time and spinning it up isn't instantaneous, this process is done asynchronously. This way, even if multiple users hit the endpoint to create workers, the main thread won't be blocked. We simply created workers and then using the create worker function inside a loop, we spawned four or a specified number of workers based on the thread count. Each worker posts messages independently and through the listener, we receive data from each worker. These results are collected via promises, stored together in an array, and finally, we sum all the results from this array to get the final outcome. So, I hope you now understand how worker threads work and how we can use multi-threaded processes in Node.js. It's an excellent concept and a great opportunity to learn thoroughly. I I also provide a link to the documentation in the video description. If you want to learn in more depth, you can check the details there and grasp it easily. Thank you, everyone. In this video, I'm going to explain what Node.js is and how it actually works, but in a super simple way. If you are new to Node.js, this video is a must-watch because once you get this, everything else will start to make sense. Don't worry, I'm not going to bore you with deep technical stuff. My goal is to give you a clear picture of Node.js so you really get it. And please don't skip this thinking it's just a theory. Honestly, every upcoming Node.js video I make will rely on the foundation you build here. If your dream is to become a Node.js developer, the ideas we cover today will keep helping you again and again in your career. Jumping into Node.js code without understanding this is like trusting someone you have never ever met. All right, let's not drag this out. Let's dive right in. So, what is Node.js? The official site describes it as a free, open-source tool that runs JavaScript outside the browser. With it, developers can build servers, web apps, command-line tools, and even small scripts. It runs on Chrome's V8 engine, and there are two key things to notice here. The engine itself and what we call the runtime. Now, what's a runtime? Simply put, it's the place where code actually runs. Node.js is a runtime, not a language. That's the difference. JavaScript is the language. Node.js just gives JavaScript a home outside the browser so it can do more than just run on a web page. And why was JavaScript created in the first place? To add life to web pages. HTML gives us structure, CSS adds style, but neither can make a page react or move. That's where JavaScript comes in. It made websites interactive, dynamic, and that's why it's called a programming language. Sir Brendan Eich is the creator of JavaScript. When he started working on it, the first thing he had to do was design a programming language. But, just creating a language wasn't enough. There also needed to be something that could understand and run that language. In other words, he had to build a system that would allow the browser to take JavaScript code and execute it. Let's start with the first part, designing the language. While doing this, he had to define certain syntaxes. For example, take a function. Suppose the function name is my function, and inside its body, we write console.log. Any JavaScript developer would immediately recognize this and say, "Yes, this is a syntax of a JavaScript function." JavaScript has countless such syntaxes, just like every other programming language. This is what defines the language itself. Now, moving to the second part, how the language actually runs. For this, Brendan Eich and his team created the JavaScript engine. This engine takes in a .js file or JavaScript code and executes it. That means it programmatically runs the instructions written by the programmer. The creators of JavaScript specifically built this engine for browsers, so that whenever a browser encounters a .js file, it can execute it with the help of the engine. In simple terms, the language and its syntax are what you write inside a .js file. That's the content. The runtime is what takes that content and executes it. But, the runtime doesn't work alone. It operates within the browser's environment. A JavaScript developer writes code that interacts with the already loaded webpage. This means the code needs access to elements like HTML, body, IMG, and so on, collectively known as the DOM or Document Object Model. You can think of the DOM as a giant JavaScript object that represents the entire page. With access to the DOM, you can manipulate and change the page as you need it. So, imagine inside the browser, you have a JavaScript engine with access to the DOM. When you pass it a JavaScript file, your code through the engine gets access to the DOM and the window object. Different browsers implement different engines. Firefox uses SpiderMonkey, Chrome uses V8, Internet Explorer used Chakra, Safari uses JavaScript Core, etc. Because different runtimes behave slightly differently, JavaScript can execute differently across browsers. Hence, cross-browser issues comes in picture. Among all the engines out there, Google Chrome's V8 is one of the fastest. Inside Chrome, V8 normally runs with access to things like the DOM and the window object. But then, a developer named Ryan Dahl had a bold idea. What if we pull this powerful engine out of the browser and run it somewhere else? That way, we are no longer limited to the browser environment. We can create a brand new context. This idea completely changed the game because V8 was open source and written in C++. Ryan was able to take its code, embed it into his own C++ project, and build something new around it. That new layer gave him the ability to connect directly with the operating system, allowing access to the resource like the file system, the network, and the memory, things a browser sandbox can't provide. And that's how Node.js was born, an open source project that uses V8 at its core. But instead of being tied to the browser, its context is now the operating system. This means when Node.js runs on a server, it can freely use the machine's resources. When Node.js runs on a server, it gets access to everything on that server. By now, you can clearly understand that Node.js is not a programming language. It's actually a C++ program that we can install and run on any machine, just like other software. Since it can work directly with the operating system, it can also run on a server because a server is nothing more than a remote computer that obviously has an operating system. So, I hope there's no more confusion about what Node.js really is. If someone ever asks you, "What is Node.js?" you can simply explain, "Node.js is a JavaScript runtime powered by the Chrome V8 engine and it can run on web servers." Up to this point, we have a understood what Node.js is. Now, let's look at how it actually works. To grasp that, we first need a quick overview of how a web server operates. A web server is where back-end developers like us host web applications. A remote client, either a user's browser or a mobile app, connects to it. If the client is a browser, it generally expects HTML. In modern single-page applications, the HTML shell may already be on the client side and the server is mainly asked for data through APIs. On the other hand, if the client is a mobile app, it typically doesn't need HTML at all. It only expects structured data, usually in JSON format, delivered through API calls. So, in simple terms, from the server, we request either HTML or data. To fulfill that request, the server might internally call other servers, query databases, or interact with the file system. And after some processing, it sends back a response to the client. If you ask the server something simple, like "What is 1 + 2?" it can calculate the answer right away and return three. That's just pure CPU work, no waiting on anything else. But, if the server has to grab data from a database, call another server, or read from a file, that becomes an I/O-heavy task. Those tasks take more time, maybe 3 seconds or more. Now, the big question is, "What happens if multiple clients send requests at the same time?" Let's take Java as an example. A Tomcat server handles multiple requests using multiple threads. I won't go deep into what threads actually are, but if you are not familiar, you can think of them as tiny processors or small programs. The Tomcat server usually keeps a pool of threads ready in advance. When a new request comes in, it gets assigned to one of those threads. So, if five requests arrived at the same time, five different threads are used and they all run in parallel, returning responses right away. Sounds pretty cool, right? But, here's the catch. By default, Tomcat has 200 threads. That means it can serve up to 200 user requests at once. If more requests arrive beyond that, the extra one have to wait in line. You can increase the number of threads if you add more resources, but there's always a practical limit to how many threads you can actually run. Now, back to Node.js. JavaScript is traditionally single-threaded programming language. There is no extra threading. All the requests or tasks are handled by the one main thread. So, how can it handle multiple concurrent requests without one long IO operation blocking others? Ryan Dahl could have introduced true multithreading directly into JavaScript, but that would have fundamentally changed the nature of the programming language itself. Instead, he chose to keep a single main thread for JavaScript execution and hand off IO tasks to the operating system using asynchronous non-blocking mechanisms. Now, let's go back to our earlier scenario. We all know JavaScript is single-threaded, right? That means all requests are handled by the main thread. Imagine a user makes a request to your Node.js server, and that request takes 5 seconds to complete. The user will get the response after 5 seconds. But, what happens if another request comes in while the first one is still being processed? In that case, the second user won't get a response until after 10 seconds. The first 5 seconds are spent generally for the first response, and the next 5 seconds are used for the second one. Now, imagine 100 requests coming in at the same time. What happens then? There's still only one thread. As you can see, this quickly becomes a very complicated situation. But, does that mean Node.js is a bad choice as a web server? Not at all. If that were the case, Node.js would never have become so popular. The truth is, Node.js handles these situations in a completely different way. So, Node.js is a single-threaded runtime, and its primary job is to accept user requests. Node.js essentially says, "Why should I do all the heavy work myself? My main responsibility is to accept incoming requests. The long-running tasks can be handled by my assistants." This means the single thread in Node.js will properly receive requests, but won't block or wait while generating the output. That's the game-changing idea. Requests don't block the single thread. Now, how does Node.js achieve this? It relies on two core concepts: asynchronous operations and non-blocking I/O. Non-blocking I/O means when Node.js receives a request involving a time-consuming operation, like a database query or reading and writing large files, it passes that task to its assistants, often referred to as workers. Once the task is handed off, Node.js doesn't wait for the worker to finish. Instead, it immediately returns to listening for and accepting new requests. So, imagine user one sends a request. That request is handed to a worker for processing, and the main thread is now free again. While the worker is still busy handling user one's task, a new request from user two arrives. Because the main thread isn't stuck producing user one's response, it can smoothly accept user two's request, assign the task to another worker, and once again free itself up to keep accepting more requests. This is how Node.js manages to handle many concurrent requests efficiently, even with a single thread. So, this is non-blocking behavior of Node.js. Now, imagine this. A request needs heavy pure CPU-intensive work, for example, summing numbers from 1 to 1 million or image processing, machine learning inference, big data crunching. Focus here carefully. It's not an IO-intensive work. It's a CPU-intensive work. That means main thread has to process that request. So, once again, if the single thread itself is tied up with a heavy job, it will block other incoming JavaScript tasks. That's why Node.js isn't the best choice for CPU-bound workloads, unless you offload them or break them down into smaller parts. On the other hand, when it comes to the IO-bound tasks like database queries, network requests, or file operations, Node.js really shines. But, don't get the wrong idea. This doesn't mean Node.js can't handle CPU-intensive work. By CPU-intensive, I am referring to tasks that require very high competitive processing power. Let's return to the previous example once more. When we handed off the task to the worker, we didn't immediately get a response from them, nor did we send any response back to the user. So, what happens when a worker finished its job? This is where the concept of asynchronous or callback comes in. Once a worker completes its task, it triggers a callback. A callback is essentially a notification or event that the job is done. That's why Node.js is often described as having an event-driven architecture. Through callbacks, we can programmatically receive a notification when a worker has completed its heavy processing, and at that point, we define what happens next. This is what makes it asynchronous, meaning it doesn't block or follow the synchronous execution of the main thread, but instead maintains order and execution flow internally in its own way. I won't dive deeper into the topic of asynchronous programming right now because it's quite broad on its own. If you are interested, I can create a detailed and advanced video tutorial dedicated entirely to that subject. Just let me know in the comments if you'd like to see that tutorial. When we are learning Node.js, it's absolutely essential to talk about the event loop. This is one of the core internals that every Node.js developer needs to understand. So far, we have seen Node.js handle both synchronous and asynchronous tasks, but who's actually controlling all of that behavior? How does a single thread manage and coordinate long-running IO-intensive operations? The answer lies in the event loop. You can think of the event loop as an infinite while loop that's always running in the background, constantly checking for tasks that need attention. Its job is to keep waiting for new tasks, and one by one forward them to the appropriate place when required. To do this, it relies on several components: the task queue, the task pool, the completed task queue, and the event loop itself. The task queue holds all the pending tasks that are waiting to be executed. From there, tasks are sent into the thread pool one at a time for processing. At its core, the event loop is a simple concept to grasp, though there are many more details and deeper mechanics you can explore. If you'd like, I can create a separate in-depth video dedicated to exploring the event loop in detail, but for now, here's the key idea. The event loop runs continuously like a while loop, keeps track of long-running tasks through the task queue, assigns them to available threads in the thread pool, and when those tasks are completed, it passes the results back to the main thread. This mechanism is what makes Node.js highly available and extremely efficient as a web server. But, there's one more thing to understand. The thread pool and event loop are mainly designed to handle IO intensive tasks. As I mentioned earlier, when it comes to CPU intensive operations, the main thread can still get blocked. However, in the latest versions of Node.js, the team introduced the concept of worker threads. With worker threads, we can now perform CPU intensive tasks without blocking the main thread. This is a more advanced topic, and I already have a separate video on Node.js worker threads where I explain how to handle these kinds of CPU heavy operations. The link to that video is in the video description. By leveraging worker threads, I truly believe Node.js has the potential to expand beyond web servers and play a significant role in areas like image processing and even machine learning in the near future. Finally, you might be wondering, "If Node.js is single-threaded, then where do these workers actually come from?" The answer is libuv. It's a special library written in C++. The library leverages the operating system kernel to provide the underlying threads that handle IO operations. JavaScript itself always runs on a single main thread, but Node.js, through libuv, can handle multiple IO operations by utilizing multiple threads in the background. So, the assistants I mentioned earlier are nothing but threads working behind the scene. When a user sends a request to the server, the server itself operates as if it's running on a single thread. But, in the background, it leverages the power of multithreading through its workers. This clever and powerful design is what makes Node.js blazing fast, highly performant, and incredibly flexible. You have just navigated the deep waters of JavaScript internals. From the nuances of the call stack to the power of worker threads. You now have the conceptual blueprint that separates great engineers from the rest. This foundation is your greatest asset, not just for building high-scale applications, but for walking into any technical interview with the confidence to explain exactly why your code works the way it does. If this deep dive helped you connect the dots, I would love to hear your feedback in the comments below. What was your biggest aha moment? If you want to keep leveling up your engineering mindset, make sure to subscribe to my small Logic Base Labs YouTube channel for more deep dive tutorials. Thank you for watching and keep building, keep questioning, and keep thinking in a JavaScript way. I will see you in the next one.
Generated algorithmically for Search Engine
Indexing.