Introduction: When JavaScript Gets Tricky
Sometimes you stumble upon a tiny snippet of JavaScript and immediately feel like the language is testing your sanity: Take the example below:
Example 1:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
What do you think it prints? 1 2 3, 3 3 3, or 4 4 4?
If you guessed 1 2 3 or 3 3 3, don’t feel too bad, you’ve just fallen into JavaScript’s classic “gotcha” trap. The actual output is: 4 4 4. You may ask, what is 4 business in all of this, how on earth is 4 <= 3, well, welcome to JavaScript my friend.
Here’s another one:
Example 2:
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
What do you think it prints? 1 2 3, 3 3 3, or 4 4 4?
Surprise! It prints: 1 2 3
The difference? It’s all about how you declare your variables (var or let). var behaves like a single shared memory cell, while let creates a fresh copy for each iteration. In other words, JavaScript remembers differently depending on your choice. Sounds too technical already? Don't worry, I would break it all down for you at the end of this article.
But first, let's talk about
1. Scoping in JavaScript: Where Your Variables Live
The real culprit behind these surprises is scoping. Scoping determines where variables are accessible in your code. JavaScript utilizes lexical (or static) scoping, as opposed to dynamic scoping. What then is lexical scoping?
Lexical Scoping simply means that a variable's scope is determined by where it is written in the source code, not by where it is called at runtime.
In other words:
When a function is created, it remembers the scope in which it was defined.
It does not matter where the function is called; only where it was defined matters.
Let's take an example.
Example 3:
let x = 1;
function foo() {
console.log(x);
}
function bar() {
let x = 2;
foo();
}
bar();
Outputs
(Lexical Scoping): 1
(Dynamic Scoping) 2
Let's break down the lexical scoping concept:
foo() is called inside the function bar(), the value of x that was captured what determined by where foo() was written. As at creation of function of the function foo(), x is 1. Even though foo() is called inside bar(), a value x = 2 exists there, it doesn’t matter. As we said earlier: “It does not matter where the function is called, only where it was defined.”
Even though foo() is called inside bar(), a value x = 2 exists there, it doesn’t matter. As we said earlier: “It does not matter where the function is called, only where it was defined.”
A dynamic scoping just does the opposite of lexical scoping, that can kind of scoping technique is found in "ancient" programming languages like Lisp.
Now that we understand the scoping technique javascript uses, let's talk about the scope of javascript variable var, let and const.
var - is function-scoped (or global if declared outside a function) variable and can be accessed anywhere inside the function it was declared.
let / const - are block-scoped, only exists inside { } where declared.
Let's visualise this with an example:
Example 4:
function testVar() {
if (true) {
var x = 10;
let y = 15
}
console.log(x); // 10 → accessible outside the block to the whole function (function-scoped)
console.log(y); // throws a ReferenceError: y is not defined
}
Notice how x logs a 10 in the console? This is because even though it is defined in a block (if statement), it's scope is the function and it's accessible everywhere in the function testVar()
For let on the other hand, it throws a ReferenceError because it's scope is confined to the block (if statement) therefore, console.log can't seem to find (reference) it. const is similar to let, just that is only holds a constant value and cannot be reassigned a new value unlike let.
Remember how we are able to access var in the function scope even though it was declared inside a block? That takes us to the next concept I would be talking about, Hoisting.
2. Hoisting: JavaScript’s Magic Trick
Do you know that before your code runs, JavaScript secretly moves declarations to the top of their scope. This is called hoisting. All variable types and functions are hoisted in JavaScript, how they are done is what differs.
var → hoisted and initialised to undefined.
let / const → hoisted but cannot be accessed yet (Temporal Dead Zone)
Function declarations → fully hoisted (body included)
Function expressions → only the variable is hoisted
Let's break down the above with examples.
Example 5: var hoisting
console.log(a); // undefined
var a = 5;
console.log(a); // 5
What happens internally:
var a; // declaration is hoisted (moved to the top of it's scope)
console.log(a); // undefined (no value yet)
a = 5; // initialization happens here
console.log(a); // 5
var declarations are hoisted and initialized with undefined.
Example 6: Hoisting with let and const
console.log(b); // ReferenceError
let b = 10;
console.log(c); // ReferenceError
const c = 20;
let and const are hoisted too, but they are in a temporal dead zone (TDZ) until the line where they are declared. Accessing them before the declaration throws ReferenceError.
Example 6: Hoisting with functions
greet(); // "Hello"
function greet() {
console.log("Hello");
}
The entire function (declaration + body) is hoisted. You can call it before its declaration.
Example 7: Hoisting with function expressions:
hello // undefined
hello(); // TypeError: hello is not a function
var hello = function() { console.log("Hi"); };
Only the variable hello is hoisted (as undefined) not the function body because it is defined as a var (remember what we said about var hoisting in e.g 5). So calling it early throws an error.
Example 8:
hello // Reference Error
hello(); // Reference Error
const hello = function() { console.log("Hi"); };
This just extends what happens with let / const because it's a function expression. What happens with function expression depends on the type of variable.
Hoisting explains why some code “seems to exist before it’s declared.” Think of it as JavaScript sneakily rearranging your furniture before you enter the room. Finally, let's talk about closures.
3. Closures: Functions That Never Forget
A closure is a function that remembers variables from the scope where it was created, even after that scope is gone. Closures are created by functions, exist inside functions, and are about how functions capture variables.
Closures are why your setTimeout callbacks could remember i differently depending on var vs let.
Example 10: Counter Closure
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
Closure is what allows the function counter() to still be able to access count declared in createCounter even after it has been called.
Let's take a look at closure in loops and go back to genesis of everything, e.g 1 & 2.
// Example 1
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000); // 4 4 4
}
// Loop start
// i = 1 → schedule callback #1
// i = 2 → schedule callback #2
// i = 3 → schedule callback #3
// Loop ends, i becomes 4
// Essentials, var uses the same variable (i) all through.
// Returns the same i that is already 4
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000); // 1 2 3
}
/ Loop start
// Closure #1 → creates a new variable i1 → 1
// Closure #2 → creates a new variable i2 → 2
// Closure #3 → creates a new variable i3 → 3
// let creates stores a new variable (i1, i2, i3) in memory for each loop
//therefore each iteration has it's own i
4. Connecting the Dots
Lexical scoping → decides which variables a function can see
Hoisting → moves declarations to the top, creating surprises if you’re not careful
Closures → allow functions to “remember” variables, leading to powerful patterns or head-scratching bugs
Master these three concepts, and suddenly JavaScript’s “tricky snippets” start to feel predictable.
5. Conclusion
JavaScript loves to surprise us, but understanding scoping, hoisting, and closures gives you superpowers:
You’ll predict outputs accurately
You’ll write cleaner, safer code
You’ll stop wondering if JS is secretly messing with you
And remember: JavaScript isn’t trying to confuse you… it’s just teaching you to respect its rules. 😉
Top comments (0)