Hide and Seek

episode 04

#Lexical Scope

Illustration of two adventurers playing hide and seek

To execute JavaScript code in our minds, we need one fundamental skill: know which identifiers are accessible at each point of our program.

This idea is known as scope, and understanding it is the final building block before learning closures.

I like to think of scope as a system that builds invisible walls around our code. From the outside you can’t peek in — anything declared inside stays private. But from the inside, you can always see the world around you.

There are a few things we need to see to acquire this knowledge.

Block Scope, Function Scope, Global Scope.

Block Scope

Illustration of a castle with the door half closed

Walls but not for everyone.

Every time we use brackets {} we generate an enclosing space for variables declared with const and let that prevents them from being visible and accessed from the outside.

What about variables declared with var? Don't worry, we leave them for later.

Variables with let and const

Any time we use curly braces {}, JavaScript creates a new block scope.

This means that variables defined inside a block are not visible from the outside.

{
  const treasure = 'ring';
}
 
console.log(treasure); // Throw ReferenceError

The reason this code throws a ReferenceError is because treasure lives inside an inner block scope, and the outer scope can't access it.

And there is more. We have seen that we can't access a variable declared inside a block {} from the outside.

But if the variable is declared outside, can we access it from an inner block?

const treasure = 'ring';
 
{
  console.log(treasure); // ?
}
Answer
Don’t reveal until you have your answer.
Answer

Well that would work perfectly.

A block can read variables that exist in an outer scope, but not the other way around. Inner scopes can "see" outer scopes, but outer scopes cannot "see" inner ones.

const treasure = 'ring';
 
{
  console.log(treasure); // 'ring'
}

Constructs that create a block scope:

  • {} simple blocks

  • if / else

  • for, while, do...while

  • switch

  • try / catch / finally

Any of these forms a new block scope for let and const.

Variables with var

Variables declared with var do not follow block scope because of how JavaScript was originally designed and how the language specification treats it.

In early JavaScript (1995), the only kind of scope that existed was function scope. Blocks {} did not create new scopes at all.

So var was designed to attach itself to the nearest function scope, not the closest block like let and const.

When JavaScript sees:

{
  var x = 1;
}

It internally asks, what is the nearest function environment?

  1. Save x there.

  2. If there is not a function around, it saves it in the global scope.

The block is completely ignored.

A little test now. What happens when we run this code?

{
  var location = 'chamber of secrets';
}
 
console.log(location); // ?
Answer
Don’t reveal until you have your answer.
Answer

JavaScript saves the variable location in the global scope ignoring the block {} and making possible for the console.log() to reach the variable location.

{
  var location = 'chamber of secrets';
}
 
console.log(location); // 'chamber of secrets'

And all these leads us to functions.

Are we missing something? Functions also use curly braces, are they creating a new block scope?

Well, yes and no.

Functions do create a new scope, but not a block scope. They create a Function Scope, which is a different kind of scope with its own rules.

Let’s break it down.

Function Scope

Illustration of a castle with the doors open

Everyone is welcome inside this walls.

A function scope is much stronger and more isolated than a block scope. Everything declared inside a function—whether it’s var, let, const, parameters, or even inner function declarations—lives exclusively inside that function’s private environment. Nothing leaks out unless you deliberately return it.

Let’s look at a simple example:

function hunt() {
  const treasure = 'gold';
  let map = 'old map';
  var coordinates = '1234';
}
 
console.log(treasure); // Throw ReferenceError
console.log(map); // Throw ReferenceError
console.log(coordinates); // Throw ReferenceError

Even var, which happily ignores block scope, is completely contained inside a function. That’s because functions don’t create a block scope — they create a function scope, and var is designed to belong to whichever function scope is nearest.

In other words

A function forms a boundary. Anything declared inside stays inside.

And just like with block scopes, the relationship is one-directional:

  • From inside the function you can access things declared outside.

  • But the outside world can’t see anything declared inside the function.

Global Scope

Illustration of a meadow

You can’t fence in the wind. No walls at all.

We only need to remember two things about the Global Scope:

  • If you declare something outside of any function or block, it belongs to the global scope.
  • And everything inside your program can “see” it.
const globalTreasure = 'ring';
 
function cave() {
  console.log(globalTreasure); // 'ring'
}
 
{
  console.log(globalTreasure); // 'ring'
}
 
cave();

No matter where you are—in a function, in a block, or at the top level—the global scope is always available as a fallback. There are no walls here; everything is accessible from anywhere.

Recap

  • Scope defines which identifiers are accessible at each point in a program.
  • JavaScript uses scope boundaries to control visibility and prevent unwanted access to variables.
  • Block scope is created by {} and applies to variables declared with let and const.
  • Variables declared with var do not respect block scope and attach to the nearest function scope instead.
  • Function scope creates a private environment where all declarations (var, let, const, parameters, and inner functions) are isolated.
  • The global scope contains identifiers declared outside any function or block and is accessible from anywhere in the program.
  • Scope visibility is one-directional: inner scopes can access outer scopes, but not the other way around.