DEV Community

Cover image for Do you know how it works? - JS Scopes
Matheus Julidori
Matheus Julidori

Posted on • Edited on

Do you know how it works? - JS Scopes

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, and const declared inside a function).
  • Block scope is defined by blocks like if, for, and while — but only applies to let and const.
  • 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 — prefer let and const.
  • 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();
Enter fullscreen mode Exit fullscreen mode

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 and let 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();
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Let’s break this down:

  • The main() function’s scope knows about num1, num2, res, and the calcPower() function because the first three are declared within main(), and calcPower() is declared in the global scope.
  • If we uncomment console.log("calcSum: " + calcSum);, we’ll get an error — because calcSum() was declared inside otherScope() and isn’t visible to main().
  • The calcPower() function knows about a, b, and res, since they are all declared within its own function scope.
  • If you uncomment console.log("res: " + res); in calcPower(), it will throw an error — because res from main() is not visible inside calcPower() (they're two different res variables).
  • The same applies to num1 — it only exists inside main(). The variable a holds a copy of the value of num1 passed as an argument — same value, different variable.
  • The for loop block "knows" everything from its parent (calcPower()), including a, b, and res.
  • But the parent scope cannot access i, because i was declared using let inside the for loop — it only exists there.
  • The nested printAfterLoop() function also has access to res, a, and b because it's declared within the calcPower() scope.
  • But printAfterLoop() does not have access to i, because i only exists in the for loop — sibling scopes don’t share values.
  • Finally, the res value from calcPower() is returned and assigned to the res variable in main() — because the result was returned and assigned explicitly. It’s a copy of the value, just like when main() passed num1 and num2 to a and b.

Scopes

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
michael_liang_0208 profile image
Michael Liang • Edited

Nice post. What is the best practice for declaring variables?

Collapse
 
matheusjulidori profile image
Matheus Julidori

Thanks! Regarding the variables, that's a very deep question.
If you mean regarding var, let and const, my opinion is:

  1. Use const by default. It reduces bugs and improves readability, because it doesn't make objects immutable, but prevents rebinding.
  2. If you NEED to reassign a value to a variable, use let. Use it only when a variable’s value changes over time (for loop counters, accumulators, temp variables, etc).
  3. Avoid 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:

  • Minimize scope: Declare variables as close as possible to where they're used. Keeps code tight and easier to understand and reduces cognitive load.
  • Don’t group all declarations at the top of a function. That’s a 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)
  • Avoid undeclared variables. Always use strict mode ('use strict';) or modules, which enforce declaration.
  • Avoid using non-descriptive names (thing, data, item), it hurts readability and maintainability. Use names that communicate the purpose of the variable.
  • On booleans, use names that suggest a bool value (isOpen, isActive, hasValue).
  • Clear names are better than small names. Don't be affraid to have a variable named "amountOfPointsSpentOnSprintWeekly", it might look big, but you look at it and you know exaclt what the variable is. It wouldn't be the case if the variable was names "ptsSprintWeek". For someone who does'nt know the context of the code, the bigger one has more to it.
Collapse
 
michael_liang_0208 profile image
Michael Liang

Wow, thanks

Collapse
 
matheusjulidori profile image
Matheus Julidori

Thank mate! Feel free to contact me via contact@julidori.dev

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

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

Collapse
 
matheusjulidori profile image
Matheus Julidori

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.