DEV Community

Cover image for 🪄 The Secret Life of JavaScript: Scopes, Closures & Function Sorcery
Anik Sikder
Anik Sikder

Posted on

🪄 The Secret Life of JavaScript: Scopes, Closures & Function Sorcery

JavaScript is not just a language, it’s a stage 🎭 where functions are the actors, scopes are the stage lights 🎥, and closures are the secret scripts they carry in their pockets.

Today, we’re sneaking past the curtain 🎟️, into JS’s backstage, and discovering how these things really work.

By the end, you’ll see why JS sometimes feels like a reality-bending trickster language and you’ll be able to pull off the same tricks yourself.

Ready? Popcorn 🍿 in hand? Let’s dive in.


🎯 Part 1: Scopes, The Treasure Map 🗺️ of Variables

Imagine JavaScript as a mansion 🏰 full of rooms.
Each room is a scope (function, block, or module).

When you say x, JS starts looking for it in the drawers of your current room.
If it doesn’t find it there, it goes room by room until it either finds x… or screams ReferenceError in despair.

This is called the Scope Chain.

👉 Example:

let x = "Global";

function outer() {
  let x = "Enclosing";
  function inner() {
    let x = "Local";
    console.log(x);
  }
  inner();
}

outer();
Enter fullscreen mode Exit fullscreen mode

🖨️ Output:

Local
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, JS is doing this:

inner scope → outer scope → global scope
Enter fullscreen mode Exit fullscreen mode

If it never finds x, it gives up dramatically:

“I checked every room! Your socks are gone!” 🧦


🧩 Behind the Scenes: Lexical Scope

JS doesn’t search dynamically, it already knows the treasure map when the code is written.
This is called lexical scope: scopes are determined by where you wrote the code, not where you call it from.


🔄 Part 2: let, const and the Magic of Block Scope

JS used to have only var, which is function-scoped.
Then ES6 came along and brought let and const, which are block-scoped.

if (true) {
  let secret = "shh!";
  console.log(secret); // ✅ works
}
console.log(secret); // ❌ ReferenceError
Enter fullscreen mode Exit fullscreen mode

Block scope means even curly braces {} create a private little drawer just for those variables.


🪢 Part 3: Closures, Backpacks 🎒 for Variables

Closures are the reason JS feels like a magician 🧙.

They let a function remember variables from its birth environment even after that environment is gone.

Think of closures as little backpacks 🎒 your function carries with it.

👉 Example:

function makeMultiplier(n) {
  return function (x) {
    return x * n;
  };
}

const times3 = makeMultiplier(3);
const times5 = makeMultiplier(5);

console.log(times3(10)); // 30
console.log(times5(10)); // 50
Enter fullscreen mode Exit fullscreen mode

Even though makeMultiplier is done running,
times3 still remembers n = 3.
That memory is stored inside its closure backpack.


🧠 How Closures Really Work

JS doesn’t take a snapshot of variables.
It keeps a live reference to them.

function outer() {
  let arr = [1, 2, 3];
  return function () {
    return arr;
  };
}

const func = outer();
console.log(func()); // [1, 2, 3]
func().push(4);
console.log(func()); // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

The array arr lives on, hidden inside the closure’s backpack.
Mutate it, and the inner function sees the change, because they share the same memory.

Closures aren’t copying values.
They’re keeping the drawers open forever.


🚨 Gotcha: Closures in Loops

Closures can also cause weird bugs if you’re not careful.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
Enter fullscreen mode Exit fullscreen mode

Output:

3
3
3
Enter fullscreen mode Exit fullscreen mode

Because var is function-scoped, all three callbacks share the same drawer.
By the time they run, i = 3.

Solution: use let (block scope creates a new drawer each time):

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
Enter fullscreen mode Exit fullscreen mode

Now you get:

0
1
2
Enter fullscreen mode Exit fullscreen mode

🧰 Closures in the Wild

Closures power half the magic in modern JS:

  1. Private variables (data hiding):
function createBank() {
  let balance = 0;
  return {
    deposit(amount) {
      balance += amount;
    },
    getBalance() {
      return balance;
    }
  };
}

const bank = createBank();
bank.deposit(100);
console.log(bank.getBalance()); // 100
Enter fullscreen mode Exit fullscreen mode

balance is completely private, no outside code can touch it directly.

  1. Event handlers & callbacks
  2. Memoization & caching
  3. Factories for creating pre-configured functions

Closures = memory + function = endless possibilities.


🎭 Part 4: Function Wrappers (Poor Man’s Decorators)

JS doesn’t need a special keyword for decorators, functions can wrap other functions easily.

👉 Example:

function shout(func) {
  return function (...args) {
    return func(...args).toUpperCase();
  };
}

function greet() {
  return "hello world";
}

const loudGreet = shout(greet);
console.log(loudGreet()); // HELLO WORLD
Enter fullscreen mode Exit fullscreen mode

You just dressed greet in a new costume 🎭.
This is how middlewares, loggers, and React hooks work under the hood.


⏱️ Practical Example: Timing Any Function

function timer(func) {
  return function (...args) {
    const start = performance.now();
    const result = func(...args);
    const end = performance.now();
    console.log(`${func.name} took ${(end - start).toFixed(2)}ms`);
    return result;
  };
}

function slowTask() {
  for (let i = 0; i < 1e7; i++) {}
}

const timedSlowTask = timer(slowTask);
timedSlowTask();
Enter fullscreen mode Exit fullscreen mode

JS wraps slowTask in a closure that remembers func, calls it, and logs the time.


🎭 Stage 2: Real Decorators (Future of JS)

Modern JS is adding official decorator syntax for classes and methods:

function log(target, key, descriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args) {
    console.log(`Calling ${key} with`, args);
    return original.apply(this, args);
  };
  return descriptor;
}

class MathOps {
  @log
  add(a, b) {
    return a + b;
  }
}

const m = new MathOps();
console.log(m.add(2, 3)); // logs call + returns 5
Enter fullscreen mode Exit fullscreen mode

This is the future: native syntax for what we already do with closures.


🧠 The Big Picture

  • Scopes → The mansion’s treasure map 🗺️ (lexical search order).
  • Closures → Backpacks 🎒 that keep variables alive even after their parents are gone.
  • Function wrappers / decorators → Costume designers 🎭 that rewire functions while carrying their memory.

Once you master these three, you stop “using JavaScript” and start bending it to your will.

Top comments (0)