Namaste JavaScript, distilled
Clean revision notes for Akshay Saini's Namaste JavaScript — the foundations, stripped to the core. Execution contexts, the call stack, hoisting, scope and the scope chain, let/const and the TDZ, block scope, closures, functions, callbacks and callback hell, the event loop, the JS engine, higher-order functions, map/filter/reduce, promises, async/await, the Promise combinators, and the this keyword — in as few words as possible.
The issue
These are tight revision notes for the opening lectures of Akshay Saini's Namaste JavaScript — the free series that explains how JavaScript actually works under the hood. The goal here is the opposite of a textbook: the fewest words that still make it click. Read it before an interview, or whenever the fundamentals feel fuzzy.
How JavaScript runs
JS is synchronous (in a fixed order) and single-threaded (one command at a time). Everything happens inside an execution context — think of it as a sealed box holding the info about the code currently running.
That box has two parts. The memory component (also called the variable environment) stores every variable and function as a key-value pair. The code component (the thread of execution) runs the code one line at a time.
var n = 2;
function square(a) {
return a * a;
}
// Memory component (key -> value)
// n -> 2
// square -> { full function code }
//
// Code component (runs line by line)
// line 1: n = 2
// line 2: square stored, nothing to run yetTwo phases, every time
When a program runs, JS creates a global execution context in two phases. The memory phase comes first: JS scans the whole file and reserves space — variables are set to undefined, functions store their entire code. Only then does the code execution phase run, top to bottom, filling in the real values.
var n = 2;
function square(a) {
return a * a;
}
var square2 = square(n);
var square4 = square(4);
// Memory phase reserves:
// n -> undefined, square -> {full code},
// square2 -> undefined, square4 -> undefined
//
// Code execution phase then runs and fills in the real values:
// n -> 2, square -> {full code},
// square2 -> 4 (square(2)),
// square4 -> 16 (square(4))The call stack
Every function call creates its own execution context, which runs the same two phases. The call stack keeps track of where JS is: it pushes a context when a function is called and pops it when the function returns. When return is hit, control goes back to the calling line and that function's context is deleted. When the whole program finishes, the global context is popped off too and the stack is empty.
Hoisting
Hoisting is the ability to use variables and functions before they appear in the code, without an error. There's no magic — it's just the memory phase doing its job before any code runs. The rule: a var is available as undefined, and a function declaration is available in full.
getName(); // Namaste JavaScript (whole function is in memory)
console.log(x); // undefined (var, not yet assigned)
var x = 7;
function getName() {
console.log("Namaste JavaScript");
}Remove the declaration entirely and it's a different story — the name was never put in memory, so JS throws and execution stops:
console.log(x); // ReferenceError: x is not defined -> execution stops
// 'x' was never declared, so it isn't in memory at all.The one exception: function expressions
A function assigned to a variable behaves like a var, not a function. During the memory phase it's just undefined, so calling it early means calling undefined() — a TypeError.
getName(); // Uncaught TypeError: getName is not a function
var getName = function () {
console.log("Namaste JavaScript");
};
// getName is a var -> 'undefined' in memory -> undefined() throws.
// Move the call below the assignment and it works fine.Every function gets its own memory
Because each function call spins up its own execution context, variables declared inside a function are local to it. Two functions can use the same name and never collide — each x lives in a separate box, and the global x is untouched.
var x = 1;
a(); // 10
b(); // 100
console.log(x); // 1 (global x is untouched)
function a() {
var x = 10; // local to a()
console.log(x);
}
function b() {
var x = 100; // local to b()
console.log(x);
}The shortest program isn't empty
Even an empty file makes JS do real work: it still creates the global execution context. In the browser it also creates a global object — window — packed with built-in functions and variables, plus a this that points to it. At the global level, this === window. Any variable you declare in global scope gets attached to that global object.
var x = 10;
console.log(x); // 10
console.log(this.x); // 10 (this === window at the global level)
console.log(window.x); // 10 (globals attach to the global object)undefined vs not defined
These look similar but mean different things. undefined is a real placeholder value: the variable was declared (so it exists in memory) but hasn't been assigned yet. not defined means the variable was never declared at all — so accessing it throws a ReferenceError.
console.log(x); // undefined (declared, not yet assigned)
var x = 25;
console.log(x); // 25
console.log(a); // ReferenceError: a is not defined (never declared)Scope chain and lexical environment
A function can use variables from its own scope and from every scope it sits inside, all the way up to global. That ladder of lookups is the scope chain. The formal version: a lexical environment is a function's local memory plus a reference to its parent's lexical environment — and "lexical" just means where the code is physically written.
function a() {
c();
function c() {
console.log(b); // 10 -> not here, not in a(), found in global
}
}
var b = 10;
a();
// A local b would win over the global one (it's found first).
// But the reverse never works:
function outer() {
var secret = 42;
}
outer();
console.log(secret); // ReferenceError: secret is not definedlet, const, and the Temporal Dead Zone
let and const are hoisted too — but unlike var they aren't set to undefined. They sit in the Temporal Dead Zone (TDZ) from the moment they're hoisted until the line that assigns them. Touch them in that window and you get a ReferenceError, not undefined. They also don't attach to the global object.
console.log(a); // ReferenceError: Cannot access 'a' before initialization
console.log(b); // (var) would be undefined
let a = 10;
var b = 15;
// And they aren't attached to window / this:
console.log(window.b); // 15
console.log(window.a); // undefined (let lives outside the global object)Block scope and shadowing
A block is anything inside { }. let and const are block-scoped — they only exist inside the block they're declared in — while var ignores blocks entirely and leaks out.
{
var a = 10;
let b = 20;
const c = 30;
}
console.log(a); // 10 (var leaked out of the block)
console.log(b); // ReferenceError: b is not definedWhen an inner name matches an outer one, the inner declaration shadows the outer. With let/const the block keeps its own copy and the outer value survives; with var there's really just one variable, so the inner write changes the outer too. And you can't shadow a let with a var in the same scope — that's illegal shadowing.
let a = 100;
{
let a = 10; // a separate, block-scoped 'a'
console.log(a); // 10
}
console.log(a); // 100 (outer 'a' untouched)
// Illegal shadowing — a var can't shadow a block-scoped let:
let x = 20;
if (true) {
var x = 30; // SyntaxError: Identifier 'x' has already been declared
}Closures
A closure is a function bundled together with its lexical scope. Because lookups walk the scope chain, an inner function keeps a live reference to its outer function's variables — and holds onto them even after the outer function has returned.
function x() {
var a = 7;
function y() {
console.log(a); // y closes over a
}
return y;
}
var z = x(); // x() has finished and is gone...
z(); // 7 — but z still remembers aInterview classic: setTimeout in a loop
Time, tide, and JavaScript wait for none. The callback inside setTimeout forms a closure over its surroundings; setTimeout stores it with a timer and moves on without waiting. So this prints the string first, then the value after the delay:
function x() {
var i = 1;
setTimeout(function () {
console.log(i);
}, 3000);
console.log("Namaste JavaScript");
}
x();
// Namaste JavaScript (immediately)
// 1 (after 3 seconds)Now the famous trap — print 1, 2, 3, 4, 5 one second apart. The obvious loop with var prints 6 five times instead:
function x() {
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
}
x();
// 6 6 6 6 6 — not 1 2 3 4 5Why? Every callback closes over the same i — a reference, not a copy. The loop finishes long before the first timer fires, leaving i at 6. The clean fix is let, which is block-scoped, so each iteration creates a brand-new i for its callback to remember.
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i); // 1, 2, 3, 4, 5 — one per second
}, i * 1000);
}
// Forced to use var? Wrap it so each call gets a fresh copy of i:
for (var j = 1; j <= 5; j++) {
(function (j) {
setTimeout(() => console.log(j), j * 1000);
})(j); // a new j per iteration — also 1..5
}The many forms of a function
A function can be written several ways, and each form has a name worth knowing. The first split is statement vs expression. A function statement (declaration) is the named function foo() {} form; a function expression assigns a function to a variable. The difference that bites is hoisting: a declaration is fully hoisted and callable before its line, while an expression is just a var — undefined until its line runs.
a(); // "Hello A" — declaration, hoisted fully
b(); // TypeError — b is still undefined here
function a() { console.log("Hello A"); } // statement
var b = function () { console.log("Hello B"); }; // expression
// A function with no name is anonymous; give the expression a name
// and it's a "named function expression":
var c = function () { /* anonymous */ };
var d = function named() { /* named */ };Drop the name and you have an anonymous function. On its own it's a SyntaxError — a statement requires a name — so anonymous functions only appear where a function is used as a value (like the expression above). Give that expression a name and it becomes a named function expression, but the name lives only inside the function, not in the outer scope.
var b = function xyz() {
console.log("b called");
};
b(); // "b called"
xyz(); // ReferenceError: xyz is not defined (name isn't in the outer scope)
// Higher-order functions: take a function in, or return one out.
function hof(fn) {
return function () { return fn(); }; // returns a function
}// Define a function and run it on the spot — no name, no later call:
(function () {
console.log("runs immediately");
})();
// Arrow form, same idea:
(() => {
console.log("also immediate");
})();
// It can take arguments and return a value, too:
var sum = (function (a, b) {
return a + b;
})(2, 3); // 5Wrap a function in parentheses and call it on the spot and you have an Immediately Invoked Function Expression (IIFE). The parentheses turn the declaration into an expression, and the trailing () runs it the instant it's defined. The point is a private scope: variables declared inside an IIFE never leak to the outside — the classic way to avoid polluting the global namespace before block scope and modules existed.
Two more terms, often mixed up: parameters are the labels in the definition; arguments are the actual values you pass at call time. And whichever form they take, functions in JS are first-class citizens — you can pass them as arguments and return them from other functions, which is exactly what makes callbacks and closures possible.
Callbacks and event listeners
A callback is a function you pass into another function so it can be called later. It's how JavaScript — single-threaded by nature — does work that happens out of order: you hand setTimeout a function and a delay, the synchronous code runs first, and the callback fires afterwards.
setTimeout(function () {
console.log("timer");
}, 5000);
function x(y) {
console.log("x");
y(); // y is the callback, invoked inside x
}
x(function y() {
console.log("y");
});
// Output: x y timer (timer comes last, after 5s)Everything runs through the call stack, and there's only one. So a slow, synchronous operation blocks the main thread — the whole page freezes until it finishes. The fix is to push time-taking work through async APIs like setTimeout so the stack stays free.
Event listeners are just callbacks attached to DOM events. addEventListener registers a handler without overwriting existing ones, and that handler forms a closure — here over count — so it keeps state between clicks.
function attachClickHandler() {
let count = 0;
document.getElementById("btn").addEventListener("click", function () {
console.log("button clicked", ++count); // closes over count
});
}
attachClickHandler();The event loop, queues, and microtasks
The call stack has no timer — it runs whatever enters it, immediately. So how does setTimeout wait? The browser hands JS extra powers through Web APIs — setTimeout, fetch, the DOM, even console.log — reached via the global window object. Async callbacks are registered with those Web APIs, not parked on the stack.
console.log("Start");
setTimeout(function cb() {
console.log("timer");
}, 5000);
console.log("End");
// Start, End, timer (timer is registered with a Web API, runs last)Step by step: Start logs, setTimeout registers cb with the Web API and starts a 5s timer, then returns immediately; End logs and the global context pops. The timer ticks outside the engine. When it expires, cb can't jump straight back onto the stack — it lands in the callback queue and waits.
The event loop is the gatekeeper: it constantly checks the queues and, the instant the call stack is empty, pushes the next callback on. Event listeners work the same way — the handler lives in the Web API environment until the event fires (or you remove it).
Promises are special. Their .then callbacks go to the microtask queue, which the event loop fully drains before it touches the callback queue — so promise work runs ahead of setTimeout and DOM callbacks (Mutation Observer callbacks are microtasks too). If microtasks keep scheduling more microtasks, the callback queue can be starved.
console.log("Start");
setTimeout(() => console.log("CB Timeout"), 0); // callback queue
fetch("https://api.example.com").then(() => console.log("CB Promise")); // microtask
console.log("End");
// Start, End, CB Promise, CB Timeout
// the promise callback jumps ahead — microtasks beat the callback queueWhy setTimeout can't promise exactly 5 seconds
A 5-second setTimeout means at least 5 seconds, not exactly 5. The callback can only run once the call stack is empty — so if your synchronous code keeps the stack busy for 10 seconds, the timer expires at 5s and the callback waits in the queue until the stack finally clears at 10s.
console.log("Start");
setTimeout(function cb() {
console.log("Callback");
}, 5000);
console.log("End");
// ...imagine a million lines of synchronous code that take ~10s here...
// The timer expires at 5s, but cb runs only after the stack is empty (~10s).This is JavaScript's concurrency model, and it's the source of setTimeout's "trust issues." The first rule follows directly: never block the main thread — there's only one call stack, so slow synchronous work freezes everything. And setTimeout(fn, 0)? Even with a 0ms timer the callback still takes the long way round — Web API → callback queue → wait for an empty stack — so it's a clean trick to defer low-priority work just past the current code.
console.log("Start");
setTimeout(() => console.log("Callback"), 0);
console.log("End");
// Start, End, Callback
// 0ms still means "after the current synchronous code finishes".Inside the JS engine (and V8)
JavaScript runs everywhere — browsers, servers, even watches — because of the JavaScript Runtime Environment (JRE): a container holding the JS engine plus the Web APIs, the event loop, and the queues. The engine is its heart. ECMAScript is the standard every engine follows — V8 (Chrome, Node), SpiderMonkey (Firefox), Chakra (old IE).
The engine isn't special hardware — it's a program (written in C++) that takes your high-level JS and runs it in three steps: parse, compile, and execute.
V8, Google's engine, names its pieces: Ignition (the interpreter), TurboFan (the optimizing compiler), and Orinoco (the garbage collector). Different vendors, same job — turn your JS into fast machine code.
V8, in kid-sized words
Forget the jargon for a second. Picture V8 as a tiny kitchen that turns your recipe (the code) into a finished meal (a running program). You hand it the recipe; it reads it, starts cooking right away, and quietly gets faster and tidier as it goes.
That's the whole engine in one breath: read it, cook it fast, make the popular dishes even faster, and keep it clean.
Now the real thing. V8 is a tiered engine: it always starts interpreting immediately, then promotes code to faster and faster machine code the more it runs. Your source flows through one pipeline:
One more trick behind the speed: hidden classes (V8 calls them Maps, or Shapes elsewhere). Objects created the same way share one hidden class, so V8 can reach a property by fixed offset instead of a dictionary lookup — and inline caches remember where that property lived last time. The practical rule: give objects their properties in the same order and don't bolt on new ones later, or you spawn new hidden classes and lose the fast path.
Higher-order functions and functional programming
A higher-order function (HOF) is simply a function that takes another function as an argument and/or returns one. The function passed in is the callback — you've been using HOFs all along (setTimeout, addEventListener, map).
function greet() { console.log("Hi"); }
function caller(fn) {
fn(); // caller is the higher-order function
}
caller(greet); // "Hi" — greet is the callbackWhy it matters: it keeps you DRY. Say you need the area of each radius — easy. Then you also need the circumference, and you copy the whole loop with one line changed. Two near-identical functions is a smell. Instead, extract the logic into its own function and pass it into a single reusable loop.
const radii = [1, 2, 3, 4];
const area = (r) => Math.PI * r * r;
const circumference = (r) => 2 * Math.PI * r;
// One loop, any operation — calculate() is the higher-order function:
const calculate = function (arr, operation) {
const output = [];
for (let i = 0; i < arr.length; i++) {
output.push(operation(arr[i]));
}
return output;
};
calculate(radii, area); // [3.14, 12.57, 28.27, 50.27]
calculate(radii, circumference); // [6.28, 12.57, 18.85, 25.13]That calculate is exactly what map does — radii.map(area) is the same idea. You can even bolt it onto Array.prototype so it's called like a built-in method:
Array.prototype.calculate = function (operation) {
const output = [];
for (let i = 0; i < this.length; i++) {
output.push(operation(this[i]));
}
return output;
};
radii.calculate(area); // [3.14, 12.57, 28.27, 50.27] — works like mapmap, filter, reduce
Three higher-order array methods that replace most hand-written loops. map transforms every element into a new array of the same length; filter keeps only the elements that pass a test; reduce collapses the whole array down to a single value.
const arr = [5, 1, 3, 2, 6];
// map: transform each element
const doubled = arr.map((x) => x * 2); // [10, 2, 6, 4, 12]
// filter: keep the ones that pass the test (odd numbers here)
const odds = arr.filter((x) => x % 2); // [5, 1, 3]const arr = [5, 1, 3, 2, 6];
// reduce: fold the array into one value.
// (accumulator, current) plus a starting value as the 2nd argument.
const sum = arr.reduce((acc, cur) => acc + cur, 0); // 17
const max = arr.reduce((acc, cur) => (cur > acc ? cur : acc), 0); // 6Because map and filter each return a new array, you can chain them — read it left to right like a sentence.
const users = [
{ firstName: "Alok", age: 23 },
{ firstName: "Ashish", age: 29 },
{ firstName: "Pranav", age: 50 },
];
// "Names of everyone under 30":
const names = users
.filter((u) => u.age < 30)
.map((u) => u.firstName); // ["Alok", "Ashish"]The dark side of callbacks: callback hell
Callbacks are essential for async work, but lean on them and two problems appear — callback hell and inversion of control. Both are the reason promises exist.
Picture an e-commerce checkout: create the order, then proceed to payment (which needs the order), then show a summary, then update the wallet — each step depends on the one before. With callbacks you nest each step inside the previous one's callback, and the code drifts rightward:
createOrder(cart, function (orderId) {
proceedToPayment(orderId, function (paymentInfo) {
showOrderSummary(paymentInfo, function (balance) {
updateWallet(balance);
});
});
});
// each step nested in the last — the code grows sidewaysThe subtler problem is inversion of control. When you pass proceedToPayment as a callback to createOrder, you hand a critical piece of your code to another function and trust it to call your callback — once, at the right time. What if it never calls it, calls it twice, or was written by someone else and breaks? You've lost control of your own flow.
Promises
A promise is a placeholder for a future value — an object representing the eventual completion (or failure) of an async operation. Instead of passing a callback into createOrder, you have it return a promise, and you attach your callback to that promise with .then.
const promise = createOrder(cart); // returns a promise immediately
// .then's callback fires automatically once the data is ready:
promise.then(function (orderId) {
proceedToPayment(orderId);
});A real promise object carries a state (pending → fulfilled or rejected) and a result (undefined until it settles). fetch returns one: it fires off the request and hands you a pending promise instantly, without blocking.
Why this beats callbacks: you attach a callback instead of passing one. The promise guarantees it calls your callback once, and only once, when the data is ready — so inversion of control is gone. Promises are also immutable: once settled, the value can't be tampered with.
const user = fetch("https://api.github.com/users/alok722");
console.log(user); // Promise { <pending> }
user.then(function (data) {
console.log(data); // runs once the response is ready
});Creating promises, chaining, and errors
On the producer side, you build a promise with new Promise((resolve, reject) => …). JS hands you resolve and reject — call resolve(value) on success, reject(error) on failure.
function createOrder(cart) {
return new Promise(function (resolve, reject) {
if (!validateCart(cart)) {
reject(new Error("Cart is not valid")); // failure
return;
}
const orderId = "12345"; // e.g. from the database
resolve(orderId); // success
});
}On the consumer side, .then handles success and .catch handles failure. A rejected promise (or a thrown error) skips ahead to the nearest .catch.
That return is the whole trick behind promise chaining: whatever you return from one .then becomes the input to the next. Forgetting to return is the classic pitfall — the chain breaks. One .catch at the end catches a rejection anywhere above it; put a .catch mid-chain if you want the rest to run even after a failure.
createOrder(cart)
.then(function (orderId) {
return proceedToPayment(orderId); // return → next .then's input
})
.then(function (paymentInfo) {
return showOrderSummary(paymentInfo);
})
.then(function (balance) {
return updateWallet(balance);
})
.catch(function (err) {
console.log(err); // any rejection above lands here
});async / await
async/await is syntactic sugar over promises — the same machinery, written so it reads top to bottom like synchronous code. An async function always returns a promise: return a plain value and it's wrapped in a resolved promise; return a promise and it's handed back as-is.
async function getData() {
return "Namaste JavaScript";
}
getData(); // Promise { <fulfilled>: 'Namaste JavaScript' }
getData().then((v) => console.log(v)); // "Namaste JavaScript"await — usable only inside an async function — pauses that function until the promise settles, then hands back the resolved value directly, no .then needed. Using await outside an async function is a SyntaxError.
const p = new Promise((resolve) =>
setTimeout(() => resolve("done!"), 3000),
);
async function handle() {
const val = await p; // function pauses here until p settles
console.log(val); // "done!" — after 3s, no .then needed
}
handle();The key behaviour: at await the function looks like it's waiting, but JS is never blocked. The function is suspended and taken off the call stack, so the stack stays free and the page never freezes; when the promise resolves, the function is pushed back on and resumes from exactly where it paused.
One gotcha worth knowing: promises start running when they're created, not when they're awaited. Two promises created up front both start their timers immediately, so awaiting them one after another waits for the slower one — not the sum. To truly run them back-to-back, create each at its await.
In the real world you mostly await fetch, then await .json() (both return promises), and you swap .catch for a try/catch block:
async function getUser() {
try {
const res = await fetch("https://api.github.com/users/alok722");
const data = await res.json(); // res.json() is a promise too
console.log(data);
} catch (err) {
console.log(err); // any await above that rejects lands here
}
}
getUser();Promise combinators: all, allSettled, race, any
Four static helpers handle several promises at once — think parallel API calls. They differ in when they settle and how they treat failure. For each below, assume three calls where p1 takes 3s, p2 takes 1s, and p3 takes 2s.
Promise.all([p1, p2, p3]) waits for all to fulfill and returns their results in order — taking 3s, the slowest. But it fails fast: if any promise rejects, it rejects immediately with that error (the others keep running, but their results are discarded).
Promise.all([p1, p2, p3])
.then((results) => console.log(results)) // ['P1','P2','P3'] after 3s
.catch((err) => console.error(err)); // if p2 rejects at 1s → rejects at 1s
// All fulfilled → array of values, in input order.
// Any rejection → Promise.all rejects right away with that first error.Promise.allSettled([p1, p2, p3]) is the safe one: it waits for every promise to settle (3s) and never rejects. You get an array describing each outcome — { status, value } or { status, reason }.
Promise.allSettled([p1, p2, p3]).then((results) => console.log(results));
// [
// { status: 'fulfilled', value: 'P1 Success' },
// { status: 'fulfilled', value: 'P2 Success' },
// { status: 'rejected', reason: 'P3 Fail' },
// ]Promise.race([p1, p2, p3]) settles the moment the first promise settles — win or lose. With p2 fastest at 1s you get its value, but if the first to finish had rejected, race rejects. Promise.any([p1, p2, p3]) is similar but ignores rejections: it settles on the first fulfillment, and only if all reject does it reject — with an AggregateError (the individual errors live on err.errors).
// race → first to SETTLE (resolve or reject):
Promise.race([p1, p2, p3]).then(console.log).catch(console.error); // p2 at 1s
// any → first to FULFILL; all rejecting gives an AggregateError:
Promise.any([p1, p2, p3])
.then(console.log)
.catch((err) => console.error(err.errors)); // ['P1 Fail','P2 Fail','P3 Fail']The this keyword
this refers to an object — and which object depends entirely on how the function is called, not where it's written. Here's the whole rundown.
In the global space, this is the global object (window in a browser; it differs by runtime). Inside a regular function it's undefined in strict mode. In non-strict ("sloppy") mode, this substitution swaps that undefined for the global object — which is why a plain call reads as window there.
function x() {
console.log(this);
// strict mode → undefined
// non-strict mode → window (this substitution)
}
x(); // depends on mode
window.x(); // window — because of how it's calledInside an object method, this is the object the method was called on — the part before the dot. And call, apply, and bind let you set this explicitly, so you can borrow a method for another object.
const student = {
name: "Alok",
printName: function () {
console.log(this.name);
},
};
student.printName(); // "Alok" — this is student
// Borrow the method, set this explicitly:
const student2 = { name: "Kajal" };
student.printName.call(student2); // "Kajal" — this is student2Arrow functions are the exception: they have no own this. They take it from the enclosing lexical scope. So an arrow used as an object method points at the outer this (often window), but an arrow inside a regular method inherits that method's this.
const obj = {
a: 10,
x: () => console.log(this), // window — arrow takes the enclosing this
y: function () {
const inner = () => console.log(this); // obj — inherits y's this
inner();
},
};
obj.x();
obj.y();And in a DOM event handler, this is the element that fired the event — e.g. <button onclick="alert(this)"> alerts the button element.
One line to carry it all: JS sets up memory first (hoisting), runs the code line by line inside nested execution contexts, resolves names by walking the scope chain outward, runs async work through the event loop, and lets functions carry that scope with them — as closures, callbacks, and the promises (and async/await) that finally tame async.