Advanced JS Concepts


Lexical Scoping and Closure
Consider the code:
function a()
{
let c=10
return function b(){
console.log(c)
}
}
const func=a()
func() // 10
a()
creates a local variable called c
and a function called b()
. The b()
function is an inner function that is defined inside a()
and is available only within the body of the a()
function. Note that the b()
function has no local variables of its own. However, since inner functions have access to the variables of outer scopes, b()
can access the variable c
declared in the parent function, a()
.
If we run the code, we will notice that the console.log()
statement within the b()
function successfully displays the value of the c
variable, which is declared in its parent function. This is an example of lexical scoping, which describes how a parser resolves variable names when functions are nested. The word lexical refers to the fact that lexical scoping uses the location where a variable is declared within the source code to determine where that variable is available. Nested functions have access to variables declared in their outer scope.
A closure is the combination of a function bundled together (enclosed) with references to its lexical environment.
Let’s understand with a code:
function counter(){
let c=0;
return function(){
c++;
console.log(c)
}
}
const increment = counter()
increment() //1
increment() // 2
increment() // 3
Running the code will give output 1 2 3 and the outer function counter()
returns another function that we are storing in a variable called increment()
and we are calling it. At first glance, it might seem unintuitive that this code still works. In some programming languages, the local variables within a function exist for just the duration of that function's execution. Once counter()
finishes executing, you might expect that the c
variable would no longer be accessible. However, because the code still works as expected, this is obviously not the case in JavaScript.
The reason is that functions in JavaScript form closures. A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any variables that were in-scope at the time the closure was created. In this case, counter
is a reference to the instance of the function that is created when counter
is run. The instance of the returned function maintains a reference to its lexical environment, within which the variable c
exists. For this reason, when counter
is invoked, the variable c
remains available for use, and every time we call increment
its value keeps on incrementing by 1.
Currying
Currying is an advanced technique of working with functions. It’s used not only in JavaScript, but in other languages as well. Currying is a transformation of functions that translates a function from callable as f(a, b, c)
into callable as f(a)(b)(c)
. Currying doesn’t call a function. It just transforms it.
Let’s see in code:
// Normal function
function sum(a,b,c)
{
return a+b+c;
}
console.log(sum(1,2,3)) // 6
// Curried function
function first(a){
return function second(b)
{
return function third(c){
return a+b+c;
}
}
}
console.log(first(1)(2)(3)) // 6
Currying uses the concept of closure.
IIFE in JavaScript
A JavaScript immediately invoked function expression is a function defined as an expression and executed immediately after creation. The following shows the syntax of defining an immediately invoked function expression:
(function(){
//...
})();
If you have many global variables and functions, the JavaScript engine will only release the memory allocated for them until the global object loses its scopes. As a result, the script may use the memory inefficiently. On top of that, having global variables and functions will likely cause name collisions. One way to prevent the functions and variables from polluting the global object is to use immediately invoked function expressions.
For Example, we want to print hello and name of the person only once and only when user opens the page for the first time, we can use IIFEE for it. We will write the code like this:
(function greet(){
alert('Welcome to IIFE');
})()
Memoization
Memoization makes heavy computational processes efficient by storing computation results in cache and retrieving the same information from cache when it’s required again instead of computing it.
Let us consider a function with heave computation of finding cube of a number:
function cube(x)
{
return x**3
}
console.log(cube(5)) // 125
console.log(cube(5)) // 125
console.log(cube(5)) // 125
We can see for same value of x
we need to run the function three times which can be computationally expensive to prevent it from performing the cube operation three times we can store it in cache. The code will be:
function cube(x)
{
return x**3
}
let memo=(func)=>{
let cache={}
return function(x){
if(cache[x])
{
console.log('Returned from Cache: ',cache[x])
}else{
cache[x]=func(x)
console.log('After computing: ',cache[x])
}
}
}
let memoCube=memo(cube)
memoCube(5) // "After computing: ", 125
memoCube(5) //"Returned from Cache: ", 125
memoCube(5) // "Returned from Cache: ", 125
Here, we are using a cache
object to store the argument as a key
and it’s computed value as value
so that whenever the function is called with same argument it fetches the result from cache instead of computing it again there by saving computational expenses.
Higher Order Functions
Higher order functions (HOFs) in JavaScript are functions that can do at least one of the following:
Accept other functions as arguments.
```javascript function add(a,b,func) { let res=a+b func(res) }
add(5,7,(val)=>{console.log(val)}) // 12
* Return a function as a result.
```javascript
function add(a,b)
{
let res=a+b
return ()=>{
console.log(res)
}
}
const ans =add(5,9)
ans() // 14
Some common Higher Order Functions are
a. Array.prototype.map
let arr=[1,2,3,4]
arr.map((val)=>console.log(val*2)) // 2 4 6 8
b. Array.prototype.filter
let arr=[1,2,3,4]
let ans=arr.filter((val)=>val%2===0)
console.log(ans) // [2,4]
There are many more Higher order functions which can be explored on MDN docs.
Hoisting and Temporal Dead Zone(TDZ)
Hoisting refers to the behavior in JavaScript where variable and function declarations are moved to the top of their scope (either the global scope or the function/block scope) during the compile phase. However, only the declarations are hoisted, not the initializations.
For example,
console.log(x) // undefined
var x =10
In the above code the declaration of variable x is moved to the top but it’s initialization remains on line two so it returns undefined.
Hoisting in var, const and let:
var: Variables declared with var are hoisted and initialized with undefined. Like in the above example.
let and const: Variables declared with let and const are hoisted but are not initialized. They remain in the TDZ until their initialization. For example,
console.log(x) //Uncaught ReferenceError: Cannot access 'x' before initialization"
let x =10
Function declarations: Function declarations are fully hoisted, meaning you can call the function even before the point where it's declared in the code.
myFunc(); // "Hello!"
function myFunc() {
console.log("Hello!");
}
But now the questions arise what is TDZ?
The Temporal Dead Zone refers to the period between entering the scope (like a block or function) and the variable being declared. In this zone, any attempt to access the variable will result in a ReferenceError
. The TDZ exists for variables declared using let, const, and class before they are initialized.
Hoisting lifts variable and function declarations to the top of their scope. The Temporal Dead Zone occurs for let, const, where the variables are hoisted but cannot be accessed until they are initialized. This prevents accessing variables before their declaration.
Bind Call and Apply
These are used for function borrowing from other objects.
a. Call: Call method invokes the function by taking the object on which the method has to be executed as first argument and accepts arguments which can be passed in that method like country in printName method.
const obj={
name:"John"
}
function greet(country)
{
console.log("Hello "+this.name+" From "+country)
}
greet.call(obj,"USA") //"Hello John From USA"
b. Apply: Apply method is very similar to the call method but the only difference is that call method takes the arguments as comma separated values whereas apply method takes an array of arguments.
const obj={
name:"John"
}
function greet(country,emoji)
{
console.log("Hello "+this.name+" From "+country+emoji)
}
greet.apply(obj,["USA","đź‘‹"]) // "Hello John From USAđź‘‹"
c. Bind: Bind method is similar to the call method, but the only difference is that call method invokes the function but in case of bind it returns a new function which can be invoked later.
const obj={
name:"John"
}
function greet(country,emoji)
{
console.log("Hello "+this.name+" From "+country+emoji)
}
const myGreet=greet.bind(obj,"USA","đź‘‹")
myGreet() // "Hello John From USAđź‘‹"
Proxy and Reflect
A Proxy
object wraps another object and intercepts operations, like reading/writing properties and others, optionally handling them on its own, or transparently allowing the object to handle them.
Syntax:
let proxy = new Proxy(target,handler)
target
– is an object to wrap, can be anything, including functions.handler
– proxy configuration: an object with “traps”, methods that intercept operations. – e.g.get
trap for reading a property oftarget
,set
trap for writing a property intotarget
, and so on.
We have a set()
method to set values of target object with our own layer of validation as well as get()
method to get values. Let’s understand with an example that we have an object with age
property which cannot be a string or negative and so we can create a proxy object of it to add our validations.
const p = {
age: 60
};
const pProxy = new Proxy(p, {
get(target, prop) {
if (prop in target) return target[prop];
return false;
},
set(target, prop, value) {
if (!(prop in target)) throw new Error("Does not exist");
if (typeof value === "string") throw new Error("Wrong type");
if (value < 0) throw new Error("Invalid");
target[prop] = value;
return true;
}
});
// pProxy["age"] = -62; //Uncaught Error: Invalid"
pProxy['age']=15
console.log(p)
/* output
{
age: 15
}
*/
Reflect
is a built-in JavaScript object that provides methods for intercepting and handling operations that are usually performed on objects. It is not a constructor (so you can't use new Reflect()
), but instead, it offers a set of static methods that allow you to interact with object properties in a more controlled and predictable way.
const p = {
age: 60
};
const pProxy = new Proxy(p, {
get(target, prop) {
if (prop in target) return Reflect.get(target,prop);
return false
},
set(target, prop, value) {
if (!(prop in target)) throw new Error("Does not exist");
if (typeof value === "string") throw new Error("Wrong type");
if (value < 0) throw new Error("Invalid");
return Reflect.set(target,prop,value)
}
});
// pProxy["age"] = -62; //Uncaught Error: Invalid"
pProxy['age']=17
console.log(p)
/* output
{
age: 17
}
*/
Generators and Iterators
An object is an iterator when it implements an interface (or API) that answers two questions:
Is there any element left?
If there is, what is the element?
Technically speaking, an object is qualified as an iterator when it has a next()
method that returns an object with two properties:
done
: a Boolean value indicating whether or not there are any more elements that could be iterated upon.value
: the current element.
For example,
const arr=[1,2,3,4]
const arrIterator=arr[Symbol.iterator]()
console.log(arr[Symbol.iterator]) // function values() { [native code] }
console.log(arr[Symbol.iterator]()) // Array Iterator {}
console.log(arrIterator.next()) // {value:1,done:false}
console.log(arrIterator.next()) // {value:2,done:false}
console.log(arrIterator.next()) // {value:3,done:false}
console.log(arrIterator.next()) // {value:4,done:false}
console.log(arrIterator.next()) // {value:undefined,done:true}
Let’s create our own iterator
function myEvenIterator(limit){
let n=0
return {
next(){
if(n<=limit){
let val = n
n=n+2
return {value:val,done:false}
}else{
return {value:undefined,done:true}
}
}
}
}
const even = myEvenIterator(6)
console.log(even.next()) // {value:0,done:false}
console.log(even.next()) // {value:2,done:false}
console.log(even.next()) // {value:4,done:false}
console.log(even.next()) // {value:6,done:false}
console.log(even.next()) // {value:undefined,done:true}
Regular functions return only one, single value (or nothing). Generators can return (“yield”) multiple values, one after another, on-demand. They work great with iterables, allowing to create data streams with ease. To create a generator, we need a special syntax construct: function*
, so-called “generator function”.
function *generateNumber(){
yield 1
yield 2
yield 3
}
const num = generateNumber()
console.log(num) // [object Generator] { ... }
console.log(num.next()) // {value:1,done:false}
console.log(num.next().value) //2
console.log(num.next()) // {value:3,done:false}
console.log(num.next()) // {value:undefined,done:true}
Generator functions behave differently from regular ones. When such function is called, it doesn’t run its code. Instead it returns a special object, called “generator object”, to manage the execution. The main method of a generator is next()
. When called, it runs the execution until the nearest yield <value>
statement (value
can be omitted, then it’s undefined
). Then the function execution pauses, and the yielded value
is returned to the outer code.
How Does JS works?
Before we get into this let’s get familiar with some terms:
JS Engine: At the heart of every JavaScript execution is the JavaScript engine. Engines like Google’s V8 (used in Chrome and Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) interpret and execute JavaScript code.
The engine consists of two main components:
Memory Heap: This is where memory allocation happens. Objects, variables, and functions are stored here.
Call Stack: A data structure that keeps track of function invocations and their order of execution.
Execution Context: When JavaScript code runs, it does so within what’s called an execution context. An execution context contains information about the environment in which the current code is being executed. There are three types of execution contexts:
Global Execution Context: This is the default context. Code that is not inside any function belongs to the global context.
Function Execution Context: Whenever a function is called, a new execution context is created for that function.
Eval Execution Context: Created when the
eval()
function is invoked.Each execution context contains the following:
Variable Object (VO): Contains all variables, function declarations, and arguments.
Scope Chain: Refers to the environment where variables and functions can be accessed.
this: Refers to the context object (which varies depending on how the function was called).
Call Stack: The call stack is a data structure used by the JavaScript engine to keep track of function calls. It works in a LIFO (Last In, First Out) manner. Here’s how it functions:
When a function is called, it is pushed to the top of the stack.
When a function finishes execution, it is popped off the stack.
Event Loops: JavaScript is single-threaded, meaning it can execute only one task at a time. However, modern applications require handling multiple tasks, like network requests or file reading, without freezing the UI. It performs the following steps:
Call Stack: Executes any synchronous code.
Web APIs: Asynchronous functions like
setTimeout
,fetch
, and DOM events are handled outside of the main thread by the browser or Node.js environment (via Web APIs).Callback Queue: When an asynchronous operation completes, its callback function is pushed to the callback queue.
Event Loop: Continuously checks if the call stack is empty. If it is, it pushes the first callback from the callback queue onto the call stack.
Web APIs: JavaScript itself is not capable of handling asynchronous tasks like making network requests or interacting with the DOM. These tasks are handled by Web APIs, provided by the browser. Some common Web APIs include:
setTimeout and setInterval: For handling timers.
fetch: For making network requests.
DOM Events: For user interactions like clicks or form submissions.
Types of Queues: JavaScript has two types of task queues: the macrotask queue and the microtask queue.
Macrotasks include things like
setTimeout
,setInterval
, and events. They are added to the macrotask queue.Microtasks include promises and
process.nextTick()
in Node.js, which are added to the microtask queue.
The event loop prioritizes the microtask queue over the macrotask queue. After each execution of code on the call stack, the event loop first clears the microtask queue before moving on to the macrotask queue. This is why promises are resolved before setTimeout
callbacks, even if both are called at the same time.
Now that we learned about the terms let’s get into code:
console.log("Hello")
setTimeout(()=>{
console.log('In macro task queue');
},1000)
Promise.resolve().then(()=>{
console.log("In micro task queue");
})
console.log('Bye');
/* output
Hello
Bye
In micro task queue
In macro task queue
*/
Debounce and Throttle
Debouncing ensures that a function is executed only after a specified delay following the last occurrence of an event. If the event is triggered again within that delay, the timer resets.
It is used in:
Searching operations: Prevent making API calls for each keystroke and instead wait until the user stops typing.
Button clicking events: Prevent accidental double submissions on a form.
function debounce(fn,delay){
let id;
return function (...args){
clearTimeout(id)
id=setTimeout(()=>{
fn(...args)
},delay*1000)
}
}
function greet(name)
{
console.log(`Hello ${name}`)
}
const dbnce=debounce(greet,3)
dbnce("Alice")
dbnce("Bob")
dbnce("John")
dbnce("David")
// Output
Hello David
As we can see clearly, we called the function four times, but it only executes the last one thus implementing debouncing concept.
Throttling on the other hand, ensures that a function is executed at most once every specified interval, regardless of how many times the event is triggered during that period.
It is used:
Scroll event handling: Optimize infinite scrolling by limiting API calls.
Button clicks: Limit how frequently a button can be clicked to avoid spam.
function throttle(fn,delay){
let id=null
return function(...args){
if(id===null){
fn(...args)
id = setTimeout(()=>{
id=null
},delay*1000)
}
}
}
function greet(name){
console.log(`Hello ${name}`)
}
let thrtl=throttle(greet,2)
thrtl("Alice")
thrtl("Bob")
thrtl("David")
// Hello Alice
As we can see even if we called the function thrice in 2 seconds it gets executed only once with the output of the first function call which is Hello Alice
.
Conclusion
Thus, we come to the end of our blog where we have gone through Advanced JS topics. Hope to see you on the next blog. Happy Coding :)
Subscribe to my newsletter
Read articles from Koustav Chatterjee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Koustav Chatterjee
Koustav Chatterjee
I am a developer from India with a passion for exploring tech, and have a keen interest on reading books.