TL;DR
- Every variable or function in JavaScript lives inside a scope — global, function, or block.
- Global scope is accessible everywhere.
- Function scope is created by functions (
var
,let
, andconst
declared inside a function).- Block scope is defined by blocks like
if
,for
, andwhile
— but only applies tolet
andconst
.- A child scope (inner) has access to its parent scope, but not the other way around.
var
is function-scoped and should be avoided in modern JS — preferlet
andconst
.- Scopes are temporary — once a function finishes running, everything inside its scope disappears.
- Understanding scope is crucial to avoid weird bugs and prepare for concepts like closures (next episode!).
Introduction
Every piece of code in JavaScript lives inside something we call a scope.
Scopes define what can be accessed, where, and for how long.
Think of scopes as environments. Each environment has its own variables and functions.
Some environments can access values from others based on a few rules. Others have shorter lifespans.
And on top of that, we have the difference between var
, let
, and const
.
Scopes in Practice
We have two main types of scope in JavaScript:
- Global scope: accessible everywhere
- Local scope: created inside functions or blocks
Let’s first understand what it means to be in the global scope. It's pretty simple: everyone can see you.
For now, don’t worry about const
, let
, and var
. Let’s focus just on scope.
var varGlobal = "I'm global";
function scope1(){
var var1 = "I only exist in scope 1"
console.log("Scope 1");
console.log("var1: " + var1);
console.log("varGlobal: " + varGlobal);
}
function scope2(){
var var2 = "I only exist in scope 2"
console.log("Scope 2");
console.log("var2: " + var2);
console.log("varGlobal: " + varGlobal);
}
scope1();
scope2();
Note that both functions can access varGlobal
, because it was defined in what we call the global scope — it isn’t wrapped in any function, it's in the root of the script execution.
Now, local scope can be “split” into two types: function scope and block scope.
Function scope is created by functions. Block scope is defined (not created — careful with that wording) by code blocks like if
, for
, and while
.
-
const
andlet
have block scope, meaning they only exist within the block they were declared in. -
var
has function scope, meaning it exists throughout the entire function and cannot be redeclared within it.
It’s important to note: blocks do NOT create scope on their own — this will become clearer shortly.
Let’s take a look at local scopes in action:
function main(){
console.log("------------------Scope: main------------------");
const num1 = 3;
const num2 = 4;
let res = 0;
console.log("calcPower: " + calcPower);
console.log("num1: " + num1);
console.log("num2: " + num2);
console.log("res: " + res);
//console.log("calcSum: " + calcSum);
res = calcPower(num1,num2);
console.log("------------------Scope: main------------------");
console.log("res after execution: " + res);
}
function calcPower(a,b){
console.log("------------------Scope: calcPower------------------");
// console.log("res: " + res);
// console.log("num1: " + num1);
let res = 1;
console.log("res: " + res);
console.log("a: " + a);
console.log("b: " + b);
console.log('------------------"Scope": for loop------------------');
for(let i = 0;i<b;i++){
res *= a;
console.log("i: " + i);
console.log("res: " + res);
console.log("a: " + a);
console.log("b: " + b);
}
//console.log("i: " + i);
function printAfterLoop(){
console.log("------------------Scope: printAfterLoop------------------");
console.log("res: " + res);
//console.log("i: " + i);
}
printAfterLoop();
return res;
}
function otherScope(){
// Many other things happening
function calcSum(a,b){
return a + b;
}
// Many other things happening
}
main();
Now look closely — you’ll notice I left some console.log()
lines commented out. Let’s analyze the execution output and what would happen if we uncommented some of them. I’ll explain the naming conventions I’m using further down.
'------------------Scope: main------------------'
`calculaPotencia: f calculaPotencia()`
'num1: 3'
'num2: 4'
'res: 0'
'------------------Scope: calculaPotencia------------------'
'res: 1'
'a: 3'
'b: 4'
'------------------"Scope": for loop------------------'
'i: 0'
'res: 3'
'a: 3'
'b: 4'
'i: 1'
'res: 9'
'a: 3'
'b: 4'
'i: 2'
'res: 27'
'a: 3'
'b: 4'
'i: 3'
'res: 81'
'a: 3'
'b: 4'
'------------------Scope: printAfterLoop------------------'
'res: 81'
'------------------Scope: main------------------'
'res after execution: 81'
Let’s break this down:
- The
main()
function’s scope knows aboutnum1
,num2
,res
, and thecalcPower()
function because the first three are declared withinmain()
, andcalcPower()
is declared in the global scope. - If we uncomment
console.log("calcSum: " + calcSum);
, we’ll get an error — becausecalcSum()
was declared insideotherScope()
and isn’t visible tomain()
. - The
calcPower()
function knows abouta
,b
, andres
, since they are all declared within its own function scope. - If you uncomment
console.log("res: " + res);
incalcPower()
, it will throw an error — becauseres
frommain()
is not visible insidecalcPower()
(they're two differentres
variables). - The same applies to
num1
— it only exists insidemain()
. The variablea
holds a copy of the value ofnum1
passed as an argument — same value, different variable. - The
for
loop block "knows" everything from its parent (calcPower()
), includinga
,b
, andres
. - But the parent scope cannot access
i
, becausei
was declared usinglet
inside thefor
loop — it only exists there. - The nested
printAfterLoop()
function also has access tores
,a
, andb
because it's declared within thecalcPower()
scope. - But
printAfterLoop()
does not have access toi
, becausei
only exists in thefor
loop — sibling scopes don’t share values. - Finally, the
res
value fromcalcPower()
is returned and assigned to theres
variable inmain()
— because the result was returned and assigned explicitly. It’s a copy of the value, just like whenmain()
passednum1
andnum2
toa
andb
.
Before you correct me on the comment, yes, I'm going to talk about the deliberate mistake I made when I said that the for loop has a scope, don't crucify me.
Initial Conclusions
From all this, we can extract a few key rules:
- An inner scope knows everything from its outer scope (parent). If you have function A inside B inside C — A has access to C.
- A scope doesn’t know about sibling scopes or nested ones it doesn't contain. If you have functions A and B in the same scope, and C inside A — A doesn't know B or C scope's.
- Everyone knows what’s in the global scope.
- Function A can call function B if they are declared in the same scope.
- Function A can call function B if B was declared inside A.
- Function A cannot call function B if B lives in a scope A doesn’t have access to.
Additional info:
- A scope exists while the function that defines it is executing. After that, it disappears.
- A variable or function declaration only lives as long as the scope that contains it. Once the scope is gone — so is the declaration.
var
, let
, and const
Before anyone comes for me — yes, I said blocks define scopes. But they don’t. Let me clarify.
“But Matheus, didn’t we just see that i
doesn’t exist outside the for
loop?”
Yes, but that behavior comes from let
, not the for
block.
Since ES6 (2015), JavaScript introduced three ways to declare variables: var
, let
, and const
.
Here's how they differ:
var |
let |
const |
---|---|---|
Lives through the entire function it’s declared in | Lives within the block it’s declared in | Lives within the block it’s declared in |
Can be reassigned manually | Can be reassigned manually | Cannot be reassigned manually |
So why did i
not exist outside the loop in our earlier example?
Because it was declared with let
inside a block — and let
is block-scoped.
Want proof?
function example() {
for (var i = 0; i <= 10; i++) {
console.log(i);
}
console.log("i outside the loop: " + i); // 10
}
The i
variable still exists outside the loop, because var
has function scope, not block scope.
Now, why did I refer to blocks as “scopes” earlier?
Well — in modern JS, you’re not supposed to use var
anymore. It’s discouraged due to unpredictability in complex blocks.
Since we mostly use let
and const
today, which are block-scoped, it’s kind of okay to think of blocks as scopes — at least for learning purposes.
And one last thing — when I said const
can’t be changed manually, I meant you can’t reassign a new value to it.
That doesn’t mean its contents can’t change.
const arr = [];
arr = [5]; // ❌ Not allowed — reassignment
arr.push(5); // ✅ Allowed — value changed, but the reference is still the same
Conclusion
Scopes in JavaScript are foundational and can be hard to visualize and understand at first — but with practice, they become second nature.
The key is to understand who knows what and when values disappear.
Next up, we’re finally going to talk about about one of the most powerful (and misunderstood) topics in JS: Closures.
I’ve been holding off on that topic because it requires a solid understanding of scopes and other topics — which we now have.
See you in the next episode of Do You Know How It Works?
💬 Got a scope-related bug story?
Drop it in the comments — I’d love to hear how you debugged it!
📬 If you're enjoying this series, save it or share it with a fellow JS dev. Let’s grow together!
👉 Follow me @matheusjulidori for the next episodes of Do You Know How It Works?
🎥 Subscribe to my YouTube channel to be the first ones to watch my new project as soon as it is released!
Top comments (6)
Nice post. What is the best practice for declaring variables?
Thanks! Regarding the variables, that's a very deep question.
If you mean regarding
var
,let
andconst
, my opinion is:const
by default. It reduces bugs and improves readability, because it doesn't make objects immutable, but prevents rebinding.let
. Use it only when a variable’s value changes over time (for loop counters, accumulators, temp variables, etc).var
at any cost. It is function-scoped, hoisted, and doesn't respect block scope. These things can and probably will cause bugs.But if you want a deeper "best practice guide" regarding other things realted to variable declarations, I'd say the basics are:
var
-era habit. Instead, group by "concept". Group declarations of variables that belong to a specific block of you code, e.g. a for loop, group all variables it will need together (don't re-declare already existing variables)Wow, thanks
Thank mate! Feel free to contact me via contact@julidori.dev
pretty cool breakdown, gotta admit i still trip up sometimes with scope stuff - you think the real trick is practice or just reading docs again and again
Practice makes perfect. At some point it will become easier and almost automatic, but docs are always there to help you when you need it
On more complex projects, using a debug tool is really handy too.