Getting Comfortable with JavaScript Scope, Function Expressions, and IIFEs


As your Javascript projects grow, it's not just what you write โ it's where you write it that starts to matter.
Why does one variable work in one place but crash in another?
Why does a function behave differently inside a loop?
Welcome to the world of scope, function expressions, and IIFEs โ some of the most misunderstood, yet powerful, features in Javascript. ๐ง ๐ฅ
In this guide, we'll learn about scope (where variables live), explore how function expressions give your code flexibility, and see how IIFEs let you run private, one-time logic.
Let's get you comfortable โ and confident โ with this essential foundation. ๐ช
๐ Scope Basics: Where Variables Live in Javascript
In Javascript, scope refers to the area of your code where a variable can be accessed. You can think of scope like rooms in a house. Some items are available in every room (global scope), while others are only found in specific rooms (local scope).
๐ Global Scope
A variable declared outside of any function or block lives in the global scope. This means it's accessible from anywhere in your script (or even the browser console if you're running in the browser).
let globalGreeting = "Hello from the outside!";
function greet() {
console.log(globalGreeting); // โ
Accessible here
}
greet();
console.log(globalGreeting); // โ
Accessible here too
๐๏ธ Local Scope
Variables declared inside a function (using let
, const
, or var
) are only accessible within that function. These are local variables.
function secret() {
let secretMessage = "This stays in the function!";
console.log(secretMessage); // โ
Works
}
secret();
console.log(secretMessage); // โ Error: secretMessage is not defined
โ Tip: Prefer using local variables unless you really need a value to be global. It keeps your code safer and easier to maintain.
๐ฆ Block Scope with let
and const
Unlike var
, which is function-scoped, both let
and const
are block-scoped. That means they live only inside the nearest set of curly braces {}
.
if (true) {
let message = "Hello from inside!";
console.log(message); // โ
Works
}
console.log(message); // โ Error: message is not defined
These scoping rules are essential to understanding how data flows through your program. They help prevent bugs, keep your code modular, and are foundational for topics like closures and callbacks โ which we'll be diving into soon. ๐
โ๏ธ Variable Shadowing: When Names Collide
Now let's talk about variable shadowing, which occurs when a variable in a local scope has the same name as a variable in an outer scope (like the global one) โ the local one shadows the outer one inside that scope.
โ๏ธ Shadowing in Action
let language = "Javascript"; // global variable
function favoriteLanguage() {
let language = "Python"; // local variable with the same name
console.log(language); // "Python" โ local shadows global
}
favoriteLanguage();
console.log(language); // "Javascript" โ global remains unchanged
Inside favoriteLanguage
, the local variable language shadows the global one. The outer language
still exists โ it's just hidden from view inside the function.
โ ๏ธ Common Gotchas with Scope
Forgetting Local vs. Global Boundaries:
function foo() {
x = 10; // โ Implicit global variable! (if 'use strict' is not enabled)
}
foo();
console.log(x); // 10 โ accidentally global
๐ก Always declare your variables with let
or const
to avoid these surprises.
Expecting Access Outside Local Scope:
function calculate() {
let result = 42;
}
console.log(result); // โ Error: result is not defined
Function Expressions: Functions as Values ๐ญ
By now, we've seen function declarations โ the classic function greet() { ... }
. But Javascript gives us more flexibility through something called function expressions.
These are especially powerful because they let us treat functions like any other value: store them in variables, pass them around, return them from other functions, and more.
Let's break this down step by step. ๐งฉ
๐ง What is a Function Expression?
A function expression is when we create a function and assign it to a variable.
const greet = function() {
console.log("Hi there!");
};
greet(); // "Hi there!"
Here:
We've defined a function without a name.
That function is stored in a variable called
greet
.We then call it using
greet()
.
Yes โ in Javascript, functions are called as first-class citizens. They can be assigned to variables just like strings or numbers.
โ ๏ธ Key Differences: Function Declaration vs. Function Expression
Feature | Function Declaration | Function Expression |
Syntax | function greet() {} | const greet = function() {} |
Hoisting | โ Yes (can be called before definition) | โ No (must be defined first) |
Can be anonymous? | โ Needs a name | โ Can be anonymous |
Scope behavior | Hoisted to the top of the scope | Exists only after assignment |
๐ Functions without a name are called as Anonymous functions. Function expressions can be anonymous (no name) or named
๐คฏ Hoisting in Action
Function declarations are hoisted, meaning Javascript moves them to the top of their scope before running your code. But function expressions are not
sayHi(); // Works fine!
function sayHi() {
console.log("Hello!");
}
But this will fail:
sayHi(); // โ Error: sayHi is not a function
const sayHi = function() {
console.log("Hello!");
};
Explanation: Javascript sees the const sayHi = ...
line as a variable declaration during hoisting, but not the function assignment โ so when you try to call it early, sayHi
is still undefined
.
When to Use Function Expressions
When you want to pass a function as data (e.g., callbacks).
When you only need it temporarily and don't want to clutter the global scope.
โก IIFE: The Fire-and-Forget Function in JavaScript
We've seen functions that we define and then call later.
But what if you want a function that runs immediately, just once, right when it's defined?
Enter the IIFE โ short for Immediately Invoked Function Expression (yep, it's a mouthful ๐ ). But don't worry, it's easier than it sounds.
๐ง What is an IIFE?
An IIFE is a function expression that runs as soon as it's created.
Here's what it looks like:
(function () {
console.log("This runs right away!");
})();
๐ Output:
This runs right away!
Let's break it down:
(function() { ... })
โ This is a function expression wrapped in parentheses. The parentheses make sure Javascript treats it as an expression, not a declaration.()
โ This pair of parentheses invokes the function immediately.
So you're defining it and calling it at the same time.
๐ค Why Use an IIFE?
Now the real question โ why write a function that runs right away?
Here are two powerful reasons:
Avoid Polluting the Global Scope ๐ก
In Javascript, everything declared outside a function lives in the global scope โ meaning it's accessible from anywhere, which can quickly become a mess ๐ฌ.
An IIFE gives you a private scope.
(function () {
const secret = "shhh... ๐";
console.log("Inside IIFE:", secret);
})();
console.log(secret); // โ ReferenceError: secret is not defined
Boom ๐ฅ โ secret
is hidden. It only exists inside the IIFE, and your global scope stays clean.
This was especially useful before block-scoped variables (let
/const
) were introduced in ES6.
Run Setup Code Just Once ๐ง
IIFEs are perfect for initialisation tasks โ the kind of stuff you do once at the start of your program.
(function () {
const config = {
theme: "dark",
language: "en",
};
console.log("Config loaded โ
");
})();
This is like loading config values, registering startup logic, or setting defaults.
No need to store the function for reuse. Just run it and move on.
๐ก Bonus Use Case: Simulated Private Variables (Pre-ES6)
Before we had modules and let
/const
, IIFEs were the go-to tool for simulating private data:
const counter = (function () {
let count = 0;
return {
increment: function () {
count++;
return count;
},
getCount: function () {
return count;
},
};
})();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
Here, count
is not accessible outside the function โ it's "private"! Only increment
and getCount
can access it. ๐ผ
๐ Syntax Variants
Here are a few ways to write an IIFE (all valid):
(function () { ... })(); // classic
(() => { ... })(); // arrow function style
!(function () { ... })(); // works too (using ! to force expression context)
The outer parentheses are required โ they force Javascript to treat the function as an expression, not a declaration.
โ ๏ธ Gotchas to Avoid
Don't forget the
()
at the end โ without it, the function won't execute.Avoid making your entire codebase one huge IIFE. It defeats the purpose of modular design.
โ Best Practices for Using IIFEs
Use for initialisation, not ongoing logic.
Keep them small and focused โ think "setup and forget".
Combine them with function expressions, closures, or even modules for extra power.
๐น Arrow Functions (Intro)
A cleaner, modern way to write functions โ with a few quirks!
In ES6 (aka ECMAScript 2015), Javascript introduced arrow functions โ a new way to define functions using the =>
syntax (called the "fat arrow").
They're handy for short, simple functions, and they make your code look cleaner and easier to read (less boilerplate, more focus).
๐ง Basic Syntax
Here's how you write a regular function:
function greet() {
console.log("Hi!");
}
And here's the same function using an arrow function:
const greet = () => {
console.log("Hi!");
};
โ Shorter
โ No function keyword
โ Perfect for quick tasks
๐ Even Shorter: Implicit Return
If your function just returns a value, you can make it a one-liner โ no curly braces {}
and no return
keyword needed:
const add = (a, b) => a + b;
This is the same as:
const add = (a, b) => {
return a + b;
};
๐ง It's called "implicit return" โ because Javascript automatically returns the result of the expression.
console.log(add(2, 3)); // 5
๐ง One Parameter? Drop the Parentheses
If you only have one parameter, you can even drop the parentheses:
const greet = name => console.log(`Hi, ${name}!`);
greet("Luna"); // Hi, Luna!
But if you have zero or multiple parameters, the parentheses are required:
const sayHi = () => console.log("Hi!");
const fullName = (first, last) => ${first} ${last};
โ Gotcha: Arrow Functions Don't Have this
Arrow functions do not have their own this
. They inherit this
from the surrounding (lexical) scope.
That's good and bad โ depending on your use case.
We will explore more on the scope, lexical environment, this
keyword, etc., in a later article.
๐ Good: Useful in callbacks
const user = {
name: "Alex",
greet: function() {
setTimeout(() => {
console.log(`Hi, Iโm ${this.name}`); // this refers to user
}, 1000);
}
};
user.greet(); // "Hi, Iโm Alex"
Here, the arrow function inside setTimeout
doesn't have its own this
, so it uses this from the surrounding function โ perfect!
โ ๏ธ Bad: Not for object methods
const user = {
name: "Jane",
greet: () => {
console.log(`Hi, Iโm ${this.name}`); // โ this is NOT user
}
};
user.greet(); // Hi, Iโm undefined
Yikes. Since arrow functions don't bind their own this
, it ends up pointing to something else (likely the global object or undefined in strict mode).
๐ Avoid using arrow functions for object methods.
๐ก Best Use Cases for Arrow Functions
โ When you need a quick, one-off function
โ
As callbacks for map
, filter
, reduce
, forEach
, etc.
โ Inside promises or async functions
By learning these essentials โ scope, expressions and IIFEs, you've unlocked a new level of control over your Javascript code.
From keeping variables private with block scope to firing off immediate functions with IIFEs, you now write smarter, safer code.
Next up โ let's dive into the world of objects and arrays and see the real magic they bring to Javascript! ๐งโโ๏ธโจ๐ฆ๐ข
Subscribe to my newsletter
Read articles from Sangy K directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Sangy K
Sangy K
An Introvert Coder | Trying to be a Full-stack Developer | A Wannabe Content Creator