Functions become more powerful when combined with concepts like scope, lexical scope, closures, and the call stack.
These concepts are heavily used in:
Scope determines where a variable can be accessed in your code.
Think of scope as the visibility area of a variable.
Inside Scope
↓
Accessible
Outside Scope
↓
Not Accessible
For example:
function greet() {
const message = "Hello";
console.log(message);
}
message exists inside greet().
Trying to access it outside the function will cause an error.
The main types of scope are:
Variables declared outside functions or blocks belong to the global scope.
Example:
const name = "Samir";
function greet() {
console.log(name);
}
greet();
Output:
Samir
Visual:
Global Scope
│
├── name
│
├── greet()
│
└── other functions
greet() can access name because the variable exists in an outer scope.
const language = "JavaScript";
function showLanguage() {
console.log(language);
}
showLanguage();
Output:
JavaScript
Global variables can be accessed from many parts of the program.
However, using too many global variables should generally be avoided because they can make large applications harder to maintain.
Variables declared inside a function are only accessible within that function.
Example:
function greet() {
const message = "Hello";
console.log(message);
}
greet();
Output:
Hello
Outside the function:
console.log(message);
Output:
ReferenceError
Visual:
Global Scope
│
└── greet()
│
└── message
message belongs to the scope of greet().
A block is code inside { }.
Variables declared using:
let
const
are block-scoped.
Example:
if (true) {
const age = 21;
console.log(age);
}
Output:
21
Outside the block:
console.log(age);
Output:
ReferenceError
Visual:
if (true)
│
└── {
age
}
age only exists inside that block.
var and Block ScopeUnlike let and const, var is not block-scoped.
Example:
if (true) {
var age = 21;
}
console.log(age);
Output:
21
But:
if (true) {
let score = 90;
const name = "Samir";
}
console.log(score);
console.log(name);
Both produce:
ReferenceError
This is one reason modern JavaScript generally prefers let and const over var.
JavaScript searches for variables from the current scope toward outer scopes.
Example:
const country = "Nepal";
function outer() {
function inner() {
console.log(country);
}
inner();
}
outer();
Output:
Nepal
JavaScript searches like this:
Current Scope
↓
Parent Scope
↓
Global Scope
This process is called the Scope Chain.
const a = 10;
function outer() {
const b = 20;
function inner() {
const c = 30;
console.log(a);
console.log(b);
console.log(c);
}
inner();
}
outer();
Output:
10
20
30
For inner():
inner()
│
├── c = 30
│
└── Parent → outer()
│
├── b = 20
│
└── Parent → Global
│
└── a = 10
JavaScript first looks inside inner().
If the variable is not found, it searches outer().
If it is still not found, it searches the global scope.
Lexical Scope means scope is determined by where functions and variables are written in the source code.
A function can access variables from the scope where it was defined.
Example:
const name = "Samir";
function greet() {
console.log(name);
}
greet();
Output:
Samir
greet() can access name because it was defined inside a scope where name is available.
function outer() {
const message = "Hello";
function inner() {
console.log(message);
}
inner();
}
outer();
Output:
Hello
Visual:
Global Scope
│
└── outer()
│
├── message = "Hello"
│
└── inner()
│
└── can access message
inner() can access variables from its surrounding lexical scope.
Example:
function outer() {
const outerValue = "Outer";
function inner() {
const innerValue = "Inner";
console.log(outerValue);
console.log(innerValue);
}
inner();
}
outer();
Output:
Outer
Inner
However, the outer function cannot access variables declared inside the inner function.
function outer() {
function inner() {
const secret = "123";
}
inner();
console.log(secret);
}
outer();
Output:
ReferenceError
The scope chain works outward, not inward.
A Closure occurs when a function remembers variables from its lexical scope even after the outer function has finished executing.
Closures are one of the most important concepts in JavaScript.
function outer() {
const name = "Samir";
function inner() {
console.log(name);
}
return inner;
}
Calling outer():
const fn = outer();
Now:
fn();
Output:
Samir
The interesting part is that outer() has already finished executing.
But inner() can still access:
name
This happens because of a closure.
When we call:
const fn = outer();
Initially:
outer()
│
├── name = "Samir"
│
└── inner()
outer() returns inner():
outer()
│
└── returns inner()
Even after outer() finishes:
fn
│
└── inner()
│
└── remembers
│
└── name = "Samir"
The inner function keeps access to the variables it needs from its lexical environment.
function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
Create a counter:
const increment = counter();
Call it:
increment();
increment();
increment();
Output:
1
2
3
Why doesn't count reset to 0?
Because the returned function remembers:
count
through a closure.
Consider:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
Create two counters:
const counter1 = createCounter();
const counter2 = createCounter();
Now:
console.log(counter1());
console.log(counter1());
console.log(counter2());
Output:
1
2
1
Each call to createCounter() creates a new lexical environment.
Therefore:
counter1
↓
count = 2
counter2
↓
count = 1
They maintain separate state.
function createGreeting(name) {
return function() {
console.log(
`Hello ${name}`
);
};
}
Usage:
const greetSamir =
createGreeting("Samir");
const greetRam =
createGreeting("Ram");
Calling:
greetSamir();
greetRam();
Output:
Hello Samir
Hello Ram
Each function remembers the name passed when it was created.
Closures can be used to hide data from outside code.
Example:
function createBankAccount() {
let balance = 0;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
}
};
}
Usage:
const account =
createBankAccount();
account.deposit(100);
console.log(
account.getBalance()
);
Output:
100
But:
console.log(account.balance);
Output:
undefined
balance is private to the closure.
Closures are heavily used in:
Example:
function createHandler(message) {
return () => {
console.log(message);
};
}
const handler =
createHandler("Button Clicked");
Later:
handler();
Output:
Button Clicked
The callback remembers message.
JavaScript uses a Call Stack to track function execution.
Think of it as a stack of function calls.
It follows:
Last In
↓
First Out
This is called:
LIFO
LIFO = Last In, First Out
function one() {
two();
}
function two() {
three();
}
function three() {
console.log("Done");
}
one();
Execution starts with:
one();
Then:
Start
↓
one()
↓
two()
↓
three()
↓
console.log()
↓
Done
After each function finishes:
console.log() removed
↓
three() removed
↓
two() removed
↓
one() removed
↓
Stack Empty
At the deepest point:
┌──────────────┐
│ console.log │ ← Current
├──────────────┤
│ three() │
├──────────────┤
│ two() │
├──────────────┤
│ one() │
└──────────────┘
After console.log() finishes:
┌──────────────┐
│ three() │
├──────────────┤
│ two() │
├──────────────┤
│ one() │
└──────────────┘
After three() finishes:
┌──────────────┐
│ two() │
├──────────────┤
│ one() │
└──────────────┘
Eventually:
┌──────────────┐
│ Empty │
└──────────────┘
A Stack Overflow happens when too many function calls are added to the call stack.
A common cause is infinite recursion.
Example:
function test() {
test();
}
test();
The execution becomes:
test()
↓
test()
↓
test()
↓
test()
↓
...
Eventually JavaScript throws an error similar to:
RangeError:
Maximum call stack size exceeded
The problem is that the recursive function has no base case.
Correct:
function countdown(n) {
if (n === 0) {
return;
}
console.log(n);
countdown(n - 1);
}
countdown(5);
JavaScript provides many built-in functions and built-in objects.
They allow us to perform common tasks without creating everything ourselves.
Examples include:
Number Conversion
String Conversion
Mathematical Operations
Dates
Console Output
Timers
parseInt()Converts a value into an integer.
parseInt("100");
Output:
100
Example:
const age =
parseInt("21");
console.log(age);
Output:
21
parseFloat()Converts a value into a decimal number.
parseFloat("99.99");
Output:
99.99
Number()Converts a value into a number.
Number("100");
Output:
100
Example:
const price =
Number("500");
console.log(price);
Output:
500
isNaN()Checks whether a value becomes NaN when converted to a number.
isNaN("Hello");
Output:
true
Example:
isNaN("123");
Output:
false
In modern JavaScript, Number.isNaN() is often preferable when you specifically need to test whether a value is the actual NaN value.
Number.isNaN(NaN);
Output:
true
String()Converts a value into a string.
String(123);
Output:
"123"
Example:
const age = 21;
const text =
String(age);
console.log(text);
Output:
"21"
Strings provide many useful methods.
Example:
const text = "hello";
console.log(
text.toUpperCase()
);
Output:
HELLO
Another example:
const text = "HELLO";
console.log(
text.toLowerCase()
);
Output:
hello
JavaScript provides the built-in Math object for mathematical operations.
Math.random();
Possible output:
0.34872
Math.random() produces a number from 0 up to, but not including, 1.
Math.round(4.6);
Output:
5
Math.floor(4.9);
Output:
4
Math.ceil(4.1);
Output:
5
Math.max(
10,
20,
30
);
Output:
30
Math.min(
10,
20,
30
);
Output:
10
JavaScript provides the built-in Date object for working with dates and times.
const now =
new Date();
console.log(now);
Output:
Current Date & Time
new Date().getFullYear();
Example output:
2026
new Date().getMonth();
Months are zero-based:
0 → January
1 → February
2 → March
...
11 → December
The console object provides useful functions for development and debugging.
console.log()console.log("Hello");
Used for normal output.
console.error()console.error(
"Something went wrong"
);
Used for errors.
console.warn()console.warn(
"Warning"
);
Used for warnings.
console.table()console.table([
"Apple",
"Mango",
"Banana"
]);
Useful for displaying arrays and objects in table format.
Example:
console.table([
{
name: "Samir",
age: 21
},
{
name: "Ram",
age: 22
}
]);
Browsers and JavaScript runtimes provide timer functions for delayed or repeated execution.
setTimeout()setTimeout() runs a function once after a specified delay.
setTimeout(() => {
console.log("Hello");
}, 2000);
Output after approximately 2 seconds:
Hello
The time is provided in milliseconds.
1000 ms = 1 second
2000 ms = 2 seconds
5000 ms = 5 seconds
setInterval()setInterval() repeatedly executes a function after a specified interval.
setInterval(() => {
console.log("Running");
}, 1000);
Output:
Running
Running
Running
...
Approximately once every second.
Store the interval ID:
const interval =
setInterval(() => {
console.log("Running");
}, 1000);
Stop it using:
clearInterval(interval);
| Function | Purpose |
|---|---|
parseInt() |
Convert to integer |
parseFloat() |
Convert to decimal |
Number() |
Convert to number |
String() |
Convert to string |
Boolean() |
Convert to boolean |
isNaN() |
Check numeric conversion for NaN |
setTimeout() |
Run code later |
setInterval() |
Repeat execution |
clearTimeout() |
Cancel timeout |
clearInterval() |
Cancel interval |
Scope and closure are related, but they are not the same thing.
| Scope | Closure |
|---|---|
| Determines variable visibility | Preserves access to lexical variables |
| Controls where variables are accessible | Allows functions to remember variables |
| Based on where code is defined | Created when functions capture outer variables |
| Defines accessibility rules | Enables persistent function state |
Think of it like:
Scope
↓
Where can I access this variable?
Closure
↓
Can this function still access
the variable later?
Consider a reusable counter:
function createCounter() {
let count = 0;
return () => {
count++;
return count;
};
}
Create the counter:
const counter =
createCounter();
Use it:
console.log(counter());
console.log(counter());
console.log(counter());
Output:
1
2
3
Several JavaScript concepts work together here:
Function
↓
Function Scope
↓
Lexical Scope
↓
Closure
↓
Persistent count
Every time:
counter();
runs, JavaScript also uses the call stack to execute the function.
We can create customized functions using closures.
function createMultiplier(
multiplier
) {
return number => {
return number * multiplier;
};
}
Create functions:
const double =
createMultiplier(2);
const triple =
createMultiplier(3);
Use them:
console.log(
double(10)
);
console.log(
triple(10)
);
Output:
20
30
Why?
double
↓
remembers multiplier = 2
triple
↓
remembers multiplier = 3
This is closure in action.
const and letUse:
const name = "Samir";
let count = 0;
instead of relying on var.
This gives more predictable block scope.
Instead of:
let count = 0;
function increment() {
count++;
}
You can encapsulate the value:
function createCounter() {
let count = 0;
return () => ++count;
}
This prevents unrelated code from modifying count.
Closures are important for understanding code such as:
setTimeout(() => {
console.log(value);
}, 1000);
The callback can remember variables from the surrounding scope.
Closures also help explain many behaviors involving:
useState
useEffect
Event Handlers
Callbacks
Timers
Bad:
function test() {
test();
}
Good:
function countdown(n) {
if (n <= 0) {
return;
}
countdown(n - 1);
}
This prevents call stack overflow.
| Concept | Purpose |
|---|---|
| Scope | Determines variable visibility |
| Global Scope | Variables available from global code |
| Function Scope | Variables accessible within a function |
| Block Scope | Variables accessible within { } |
| Scope Chain | Searches from inner to outer scope |
| Lexical Scope | Scope determined by code location |
| Closure | Function retains access to outer variables |
| Call Stack | Tracks function execution |
| Stack Overflow | Too many nested function calls |
| Built-in Functions | Ready-made JavaScript functionality |
Math |
Mathematical operations |
Date |
Date and time operations |
console |
Debugging and output |
| Timers | Delayed and repeated execution |
Advanced functions are built around several important ideas.
Scope
↓
Where Variables
Can Be Accessed
Scope Chain
↓
How JavaScript
Searches for Variables
Lexical Scope
↓
Scope Determined
By Code Location
Closure
↓
Functions Remember
Outer Variables
JavaScript executes functions using:
Call Stack
↓
Tracks Function Calls
↓
Last In, First Out
A useful mental model is:
Function Created
↓
Lexical Scope
↓
Function Knows Its
Surrounding Variables
↓
Closure Can Preserve
Those Variables
↓
Call Stack Executes
The Function
For example:
function createCounter() {
let count = 0;
return () => {
count++;
return count;
};
}
const counter =
createCounter();
console.log(counter());
console.log(counter());
Output:
1
2
The important concepts are:
count
↓
Function Scope
↓
Captured by Arrow Function
↓
Closure
↓
Value Preserved
↓
Used Again Later
Understanding scope, lexical scope, closures, and the call stack is fundamental for learning React hooks, event handlers, callbacks, asynchronous JavaScript, state management, and modern frontend development.