I went through a deep dive on JavaScript , from variables and data types all the way to closures, the event loop, and async/await. Here's everything I put together, written the way I wish someone had explained it to me.
I recently sat down and did a proper, structured pass through JavaScript , not just picking things up as I went, but actually understanding the mechanics behind the language. I watched a few long-form courses, took notes as I went, and then rewrote those notes in a way that made sense to me. This article is basically those notes, cleaned up and expanded into something I can share.
If you're coming from another language, or if JavaScript has always felt a bit slippery to you, I hope this helps. I'll explain things the way I actually think about them, not the way a textbook would.
Table of contents
- What is JavaScript?
- Variables , let, const, and why we don't use var anymore
- Data types , primitive vs non-primitive
- Functions , declarations, expressions, and arrows
- Scope , where variables live and die
- Closures , the concept that unlocks everything
- The this keyword , four rules, one order
- Async JavaScript , callbacks, promises, async/await
- The event loop , how JavaScript does many things at once
1. What is JavaScript?
JavaScript is a lightweight, dynamically-typed, interpreted language that follows the ECMAScript specification , which is basically the rulebook that defines what JavaScript is allowed to do and how it has to behave. Every browser that supports JavaScript is implementing those rules.
A few things make it distinct from languages you might have used before:
What it's good at:
- Making web pages interactive
- Running directly in the browser , no compilation step needed
- Handling user events: clicks, scrolls, keypresses
- Server-side apps via Node.js
- Mobile and desktop apps via React Native and Electron
Key characteristics:
- JIT compiled , runs instantly, no ahead-of-time compile step for the developer
- Single-threaded , one thing at a time on the main thread
- Dynamically typed , types are figured out at runtime, not declared upfront
- Event-driven , responds to events rather than just running top to bottom
2. Variables
Variables store information , a user's name, the price of an item, a list of results. JavaScript has three ways to declare them and they behave differently.
const name = "Alice"; // Can't be reassigned. Use this by default.
let age = 25; // Can be reassigned. Use when the value changes.
var oldWay = "avoid this"; // Function-scoped, hoisted , causes bugs. Skip it.
💡 My rule of thumb: Start with
constfor everything. Only switch toletwhen you know the value needs to change. Never usevarin modern JavaScript , it has a different scoping behaviour that causes confusing bugs.
3. Data types , primitive vs non-primitive
This is one of those foundational things that trips people up when they first hit a bug. JavaScript has two categories of data types, and they behave completely differently when you copy them.
Primitive types
Primitives are the raw, basic values. The critical thing to understand: they are immutable and copied by value. When you assign a primitive to a new variable, you get a completely independent copy.
The primitive types are: String, Number, Boolean, Undefined, Null, BigInt, and Symbol.
let a = 10;
let b = a; // b gets a copy of the value 10
b = 20;
console.log(a); // Still 10 , changing b didn't touch a
Non-primitive types
Non-primitives , objects, arrays, and functions , are mutable and copied by reference. When you assign one to a new variable, you're not copying the data. You're copying the address where that data lives in memory. Both variables now point to the same thing.
const obj1 = { name: "Alice" };
const obj2 = obj1; // obj2 points to the SAME object in memory
obj2.name = "Bob";
console.log(obj1.name); // "Bob" , they're the same object
⚠️ Common gotcha: If you want a true independent copy of an object, you need to spread it:
const obj2 = { ...obj1 };
Type coercion and equality
JavaScript will automatically convert types in certain situations , this is called type coercion, and it's responsible for some of the language's most famous quirks.
console.log(2 + "3"); // "23" , number gets converted to a string
console.log("4" - "2"); // 2 , strings get converted to numbers
console.log("5" - true); // 4 , true coerces to 1
This is also why the equality operator matters:
0 == ""; // true , == allows coercion. Don't use this.
0 === ""; // false , === checks type too. Always use this.
✅ Simple rule: Always use
===. It checks both value and type without any coercion.
4. Functions
A function is a reusable block of code. JavaScript has three ways to write them and they all have slightly different behaviour.
// 1. Function declaration , hoisted, can be called before it's defined
function greet(name) {
return `Hello, ${name}`;
}
// 2. Function expression , not hoisted, stored in a variable
const greet = function (name) {
return `Hello, ${name}`;
};
// 3. Arrow function , shorter syntax, no own 'this'
const greet = (name) => `Hello, ${name}`;
The most important practical difference: arrow functions don't have their own this. They inherit it from the surrounding scope. This makes them the right choice inside callbacks and methods where you don't want this to change unexpectedly.
5. Scope
Scope determines where a variable is accessible from. There are three kinds:
- Global scope , variable is accessible anywhere in the program
- Function scope , variable is only accessible inside the function it was declared in
-
Block scope , variable is only accessible inside the
{ }block it was declared in. This is whatletandconstuse.
let global = "everywhere";
function example() {
let insideFunction = "only here";
if (true) {
let insideBlock = "only inside this if block";
console.log(insideBlock); // ✅ works
}
// console.log(insideBlock); // ❌ ReferenceError
}
// console.log(insideFunction); // ❌ ReferenceError
6. Closures , the concept that unlocks everything
Once this clicked for me, a lot of things about JavaScript started making sense.
A closure is what happens when a function remembers the variables from the scope it was created in , even after that outer function has finished running. The inner function carries its environment with it.
function makeCounter() {
let count = 0; // This variable lives inside makeCounter
return function () {
count++; // The returned function closes over 'count'
return count;
};
}
const counter = makeCounter();
// makeCounter() has finished running , but count is still alive
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
The count variable should have disappeared when makeCounter() finished. But because the returned function still references it, JavaScript keeps it alive. The returned function and its environment together form a closure.
🔑 Why it matters: Closures are the basis for module patterns, private variables, event handlers that remember state, and how React hooks like
useStatework under the hood.
7. The this keyword , four rules, one order
this is one of those things that seems confusing until you know the four rules. Once you know them and the order to check them, every this question becomes answerable.
The key insight:
thisis not set when you write the function. It's set at the moment the function is called. Think of it like the word "me" in English , who "me" refers to depends entirely on who is speaking.
Here are the four rules in priority order, highest to lowest:
Priority 1 - new binding (highest priority)
When you call a function with new, JavaScript creates a brand-new blank object and sets this to point at it.
function User(name) {
this.name = name; // 'this' = the brand-new object being created
}
const alex = new User("Alex");
console.log(alex.name); // "Alex"
Priority 2 - Explicit binding (call, apply, bind)
When you use .call(), .apply(), or .bind(), you manually tell the function what this should be.
function showName() {
console.log(this.name);
}
const dog = { name: "Buddy" };
showName.call(dog); // "Buddy" , you forced this = dog
-
.call()and.apply()run the function immediately -
.bind()returns a new function withthispermanently locked
Priority 3 - Implicit binding (look left of the dot)
When a function is called as a method on an object, look to the left of the dot at the moment it's called. That object is this.
const cat = {
name: "Whiskers",
meow() {
console.log(this.name); // 'this' = cat
},
};
cat.meow(); // "Whiskers" ✅
// Watch out , losing the binding
const fn = cat.meow;
fn(); // undefined ❌ , no dot, this = global
Priority 4 - Default binding (lowest priority)
If none of the above apply, this falls back to the global object (window in browsers) or undefined in strict mode.
function standalone() {
console.log(this); // window (browser) or undefined (strict mode)
}
standalone();
Arrow functions - the exception to all rules
Arrow functions don't have their own this. They inherit it from wherever they were written. This is exactly why they're used inside callbacks and class methods.
const timer = {
name: "MyTimer",
start: function () {
setTimeout(() => {
// Arrow function inherits 'this' from start()
// start() was called as timer.start() → this = timer ✅
console.log(this.name);
}, 1000);
},
};
timer.start(); // "MyTimer"
8. Async JavaScript - callbacks, promises, async/await
At its core, JavaScript is synchronous and single-threaded. But the web is full of things that take time , API calls, timers, file reads. Here's how JavaScript handles that without freezing the page.
Callbacks - the original solution
A callback is just a function you pass into another function to be called later.
setTimeout(() => {
console.log("This runs after 2 seconds");
}, 2000);
console.log("This runs immediately");
The problem comes when you need to chain async operations , each one depending on the previous. You end up with deeply nested callbacks. Developers called it "callback hell."
Promises - a better model
A Promise represents the eventual result of an async operation. It's always in one of three states: pending → fulfilled or pending → rejected.
fetch("https://api.example.com/users")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.log("Something went wrong:", error));
Chaining .then() keeps code flat and readable. Always use .catch() , it catches both rejections and any exceptions thrown inside the chain.
When you need multiple promises at once:
// Wait for ALL to complete , fails fast if any one fails
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
// Wait for all to settle, even if some fail
const results = await Promise.allSettled([req1(), req2(), req3()]);
// Return whichever resolves or rejects first
const winner = await Promise.race([slow(), fast()]);
async/await - promises with clean syntax
async/await doesn't replace promises , it's nicer syntax on top of them. An async function always returns a promise. await pauses execution inside that function until a promise settles.
async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user);
} catch (error) {
console.log("Failed to load user:", error);
}
}
The code reads top-to-bottom like synchronous code, but under the hood it's non-blocking and promise-based. This is the pattern you'll use in modern JavaScript.
9. The event loop , how JavaScript does many things at once
This is the one that made everything else make sense for me.
JavaScript is single-threaded , one main thread, one thing at a time. Yet it handles user clicks, API calls, and timers without freezing. That's the event loop's job.
Here's what's happening:
- Call stack , tracks the currently executing function. Code runs line by line, functions are pushed on when called and popped off when they finish.
-
Web APIs , the browser handles background tasks like
fetch()andsetTimeout()outside the main thread. -
Microtask queue , holds callbacks from completed Promises (
.then(),async/await). Processed first, always. - Callback queue (macrotask queue) , holds callbacks from timers and DOM events. Processed after microtasks.
The rule the event loop follows:
- Run everything synchronous on the call stack
- Run all microtasks (Promises)
- Run one macrotask (setTimeout, etc.)
- Go back to step 2
This explains the most common JavaScript output question:
console.log("1. Start");
setTimeout(() => console.log("2. Timeout"), 0);
Promise.resolve().then(() => console.log("3. Promise"));
console.log("4. End");
Output:
1. Start
4. End
3. Promise ← microtask, runs before the timeout
2. Timeout ← macrotask, runs last
Even though setTimeout has a delay of 0ms, the Promise callback still runs first because microtasks always drain before macrotasks run. The 0ms means "as soon as possible after the microtask queue is empty" , not "immediately."
Wrapping up
That's everything from my deep dive into JavaScript. It took a while for some of these concepts to really click , especially closures, this binding, and the event loop , but once they did, I started being able to reason about code rather than just guessing at it.
If something here didn't land on first read, come back to it after you've written a few async functions or hit the bug it describes. Some things only make sense once you've seen the problem they solve.
Based on my notes from:
- Codevolution , JavaScript Tutorial for Beginners
- Async JavaScript deep dive
- The event loop explained
If this helped you, drop a reaction or a comment , I'm writing more of these and your feedback helps me know what to cover next.
Top comments (0)