more text

This commit is contained in:
tarun-elango
2026-04-26 14:09:04 -04:00
parent 26810e43d0
commit be31df2d44
22 changed files with 10664 additions and 0 deletions
+670
View File
@@ -0,0 +1,670 @@
# 01. JavaScript Fundamentals
This chapter builds the language model that everything else in the browser depends on. If you do not have a precise idea of how JavaScript creates execution contexts, resolves variables, binds `this`, and links objects through prototypes, then later topics such as the browser event loop, DOM events, React rendering, or async data fetching become a collection of trivia instead of a coherent system.
For interview preparation, this chapter matters because many "advanced" frontend questions are actually fundamentals wearing different clothes. A stale closure in React is still a closure problem. A mysterious `this` value in a class method is still a function invocation problem. A performance issue caused by accidental object shape churn still starts with understanding how objects behave.
This file is self-contained, but it connects directly to:
- [02-javascript-in-the-browser.md](./02-javascript-in-the-browser.md), which explains where the browser runtime begins
- [03-dom-event-loop-rendering.md](./03-dom-event-loop-rendering.md), which builds on the call stack and async model introduced here
- [05-real-world-architecture-patterns.md](./05-real-world-architecture-patterns.md), which shows how frameworks rely on these exact language behaviors
## JavaScript as a Runtime Story, Not Just a Syntax Story
Many beginners treat JavaScript as a bag of syntax rules: variables, loops, arrays, functions, promises. That is not how experienced engineers think about it.
The better model is this:
- JavaScript is a language with a specification called ECMAScript.
- A JavaScript engine, such as V8 in Chrome, turns that specification into something executable.
- A host environment, such as the browser, gives the language practical capabilities like the DOM, timers, networking, and storage.
That separation is important.
When someone asks, "How does JavaScript work?" they may mean one of three different layers:
- how the language resolves variables and executes functions
- how the engine parses, optimizes, and runs code
- how the browser schedules callbacks and coordinates with rendering
This chapter focuses on the first layer, with a few hints toward the other two.
## Execution Context: The Unit of Running Code
JavaScript does not execute raw text directly. It executes code through execution contexts.
An execution context is the runtime record that tells the engine what code is currently running and what data that code can access. You can think of it as a frame containing:
- the current scope and its variables
- the outer lexical environment
- the value of `this`
- bookkeeping for how control returns when this work is done
### Why JavaScript Needs Execution Contexts
Imagine calling one function from another without any runtime frame. The engine would have no clean way to answer:
- which variables belong to which function call
- where to resume after a nested call finishes
- which `this` value applies to this call
- how closures should remember outer variables
Execution contexts solve that. They let JavaScript behave as though each running function has its own private workspace, even when many calls happen one after another.
### Types of Execution Contexts
At a high level, you should know three:
1. Global execution context
2. Function execution context
3. Module execution context
The global execution context is created when a script first starts running. In browsers, top-level script code historically interacted with the global object `window`, though modules behave differently and are more isolated.
Each function call creates a new function execution context. That means calling the same function three times does not reuse the exact same runtime frame; it creates three distinct calls with separate local state.
Modules also create their own top-level scope, which is one reason ES modules are safer and easier to reason about than older script tags that dumped everything into the global namespace.
### Creation Phase and Execution Phase
An interview-friendly but useful simplification is that execution contexts have two stages:
- creation phase
- execution phase
During creation, the engine determines what bindings exist in that scope. During execution, statements actually run line by line.
This is the intuition behind hoisting. Hoisting is not "the code moves upward." It is better understood as: the engine knows about bindings before it executes the body line by line.
```mermaid
flowchart TD
A[Source code loaded] --> B[Parse into syntax tree]
B --> C[Create global execution context]
C --> D[Register bindings and outer scope links]
D --> E[Execute top-level statements]
E --> F{Function called?}
F -- Yes --> G[Create function execution context]
G --> H[Bind parameters, local declarations, this]
H --> I[Execute function body]
I --> J[Pop context and return]
J --> E
F -- No --> K[Program waits for more work]
```
### Example
```js
const siteName = "LearnJS";
function greet(user) {
const message = `Hello, ${user}`;
return `${message} from ${siteName}`;
}
greet("Asha");
```
When the file starts:
- a global execution context is created
- `siteName` and `greet` bindings are registered
- top-level statements execute
When `greet("Asha")` runs:
- a new function execution context is created
- `user` is bound to `"Asha"`
- `message` is created inside that function scope
- the function can still access `siteName` because of lexical scope
This one example already connects execution context, function calls, local variables, and closure-friendly scope lookup.
## The Call Stack: Where Synchronous JavaScript Lives
The call stack is the structure that keeps track of currently active execution contexts.
If execution context is the runtime frame for a single piece of work, the call stack is the ordered pile of those frames.
### Mental Model
Think of the call stack like a stack of trays in a cafeteria:
- when a function is called, a new tray is placed on top
- the engine works on the tray at the top only
- when that function returns, the tray is removed
- control continues from the tray underneath
Because JavaScript on the main browser thread runs one call stack at a time, synchronous code is single-threaded from the perspective of your page code.
```mermaid
flowchart BT
A[third()] --> B[second()]
B --> C[first()]
C --> D[global]
```
### Example
```js
function third() {
console.log("third");
}
function second() {
third();
console.log("second");
}
function first() {
second();
console.log("first");
}
first();
```
What happens:
1. Global code runs.
2. `first()` is called, so its execution context is pushed.
3. `second()` is called, so its context is pushed.
4. `third()` is called, so its context is pushed.
5. `third()` finishes and is popped.
6. `second()` continues and then is popped.
7. `first()` continues and then is popped.
### Why This Matters in Browsers
If JavaScript is executing a long synchronous function on the main thread, the browser cannot use that same thread to run your next event handler or do its normal page work at the same moment. That is why a CPU-heavy loop can freeze the UI.
This also explains the phrase "JavaScript is single-threaded" in frontend interviews. The more precise statement is:
"Your page's main JavaScript execution on the main thread uses a single call stack. The browser can use other threads internally, but your code experiences a single-threaded execution model for synchronous main-thread work."
## Hoisting: Early Binding Knowledge, Not Literal Movement
Hoisting is one of the most misunderstood topics in JavaScript because it is often taught with cartoon explanations.
The engine does not physically move declarations to the top of your file. Instead, when a scope is created, the engine registers bindings before normal execution reaches the lines where those declarations appear.
### `var`, Function Declarations, and `let`/`const`
These behave differently because they were designed in different eras of the language.
#### `var`
`var` is function-scoped and is initialized to `undefined` during context creation.
```js
console.log(total); // undefined
var total = 3;
```
This works without throwing because the binding exists from the start of the scope, even though the assignment happens later.
#### Function declarations
Function declarations are hoisted with their function value.
```js
sayHi();
function sayHi() {
console.log("hi");
}
```
This is one reason classic JavaScript code often calls functions before they appear in the file.
#### `let` and `const`
`let` and `const` are also known to the engine during scope creation, but they are not initialized for use until execution reaches the declaration. The period before initialization is called the temporal dead zone, or TDZ.
```js
console.log(user); // ReferenceError
let user = "Mina";
```
### Why `let` and `const` Behave This Way
This design closes off an entire class of accidental bugs that `var` made easy:
- reading a variable before its real initialization
- leaking values across blocks unintentionally
- writing loops whose callbacks all share one mutable binding
The modern language moved toward safer defaults without breaking older JavaScript completely.
### A Better Interview Answer for Hoisting
Instead of saying, "JavaScript moves declarations to the top," say this:
"When a scope is created, JavaScript registers bindings before line-by-line execution starts. Different declaration forms are initialized differently, which is why `var`, function declarations, and `let`/`const` behave differently before their declaration lines."
That answer shows actual understanding instead of repetition.
## Scope: How JavaScript Resolves Names
Scope answers the question: when code uses a variable name, where does JavaScript look for it?
JavaScript uses lexical scope. That means name resolution depends on where code is written, not where it is called from.
### Lexical Scope
```js
const topic = "global";
function outer() {
const topic = "outer";
function inner() {
console.log(topic);
}
return inner;
}
const fn = outer();
fn(); // outer
```
`inner` resolves `topic` from the environment in which `inner` was defined. It does not care that `fn()` is called later from global code.
### Scope Chain
If a variable is not found in the current scope, JavaScript walks outward through enclosing lexical environments.
```mermaid
flowchart LR
A[inner scope] --> B[outer function scope]
B --> C[module or global scope]
C --> D[unresolved => ReferenceError]
```
### Block Scope vs Function Scope
`let` and `const` are block-scoped.
```js
if (true) {
const status = "ready";
}
console.log(status); // ReferenceError
```
`var` is not block-scoped.
```js
if (true) {
var legacy = "visible outside block";
}
console.log(legacy); // works
```
That difference is why `var` can create subtle bugs in loops and conditionals.
## Closures: Functions That Remember Their Surroundings
A closure is a function together with the lexical environment it can still access.
This is one of the most powerful features in JavaScript. It is also one of the most practical.
### Intuition
Imagine a function leaving the room but taking a backpack full of references to outer variables with it. That is not exactly how the engine stores it internally, but it is a useful mental model. The function can still use those outer bindings later.
```js
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
```
The outer function has finished, but `count` is still reachable because the returned function closes over it.
### Why Closures Exist
Closures are not a quirky feature bolted on top of the language. They are the natural consequence of lexical scope plus first-class functions.
If functions can be passed around like values, and if they resolve names from where they were defined, then closures are unavoidable.
### Real-World Uses of Closures
- event handlers that need access to component state
- factory functions that create specialized behavior
- memoization utilities
- module patterns that hide private data
- React hooks and render callbacks
- debouncing and throttling utilities
### The Famous Loop Example
```js
for (var i = 0; i < 3; i += 1) {
setTimeout(() => console.log(i), 0);
}
```
This logs `3` three times.
Why:
- there is one shared function-scoped `i`
- the callbacks close over that same binding
- by the time they run, the loop has finished and `i` is `3`
Using `let` fixes this because each loop iteration gets a new block-scoped binding.
```js
for (let i = 0; i < 3; i += 1) {
setTimeout(() => console.log(i), 0);
}
```
This logs `0`, `1`, and `2`.
### Closures and Memory
Closures are powerful, but they also mean data can stay alive longer than beginners expect. If a long-lived callback still references a large object, that object cannot be garbage collected.
In real apps, memory leaks often come from some combination of:
- long-lived event listeners
- timers that are never cleaned up
- caches that never expire
- closures capturing more state than necessary
That is not a reason to avoid closures. It is a reason to understand lifecycle.
## `this`: Determined by Call Site, Except When It Is Not
Few JavaScript topics cause more confusion than `this`, largely because people first learn it as if it were a variable stored inside the function definition. That is not usually how it works.
For normal functions, `this` is determined by how the function is called.
### The Four Practical Binding Rules
In everyday JavaScript, think in this order:
1. Was the function called with `new`?
2. Was it called with `call`, `apply`, or `bind`?
3. Was it called as a method through an object reference?
4. Otherwise, default binding applies.
#### Default binding
```js
function show() {
console.log(this);
}
show();
```
In strict mode, `this` is `undefined`. In sloppy older browser script mode, it may become the global object. Modern code should assume strict mode semantics, especially in modules.
#### Implicit binding through an object
```js
const user = {
name: "Ravi",
greet() {
console.log(this.name);
}
};
user.greet(); // Ravi
```
Here `this` points to the object used at the call site.
#### Explicit binding
```js
function greet() {
console.log(`Hello ${this.name}`);
}
greet.call({ name: "Lena" });
```
#### Constructor call with `new`
```js
function User(name) {
this.name = name;
}
const person = new User("Karan");
```
With `new`, JavaScript creates a fresh object, links its prototype, binds `this` to that object inside the constructor, and returns it unless something else is explicitly returned.
### Arrow Functions
Arrow functions do not get their own dynamic `this`. They capture `this` lexically from the surrounding scope.
```js
const team = {
name: "Platform",
members: ["A", "B"],
print() {
this.members.forEach((member) => {
console.log(this.name, member);
});
}
};
```
The arrow function inside `forEach` uses the `this` from `print`, which is often exactly what you want.
### Why `this` Works This Way
JavaScript was designed to support several programming styles at once:
- simple function calls
- object method calls
- constructor-style object creation
- callback-heavy event-driven code
The call-site-based `this` model gives flexibility, but the tradeoff is confusion. ES6 arrow functions were partly a repair mechanism for the most common pain point: nested callbacks losing the intended outer `this`.
### Real-World Frontend Consequence
In browser apps, `this` bugs often show up when:
- passing class methods as callbacks without binding
- using DOM event handlers and assuming `this` is some app object
- mixing arrow functions and method syntax without understanding the difference
Frameworks reduced the visibility of many `this` issues, but they did not remove the underlying rules.
## Objects, Prototypes, and Inheritance
JavaScript is prototype-based. That sentence is often memorized and rarely understood.
### The Core Idea
Objects in JavaScript can delegate property lookup to another object, called their prototype.
If you try to access `obj.value` and `value` is not found directly on `obj`, the engine checks the prototype, then the prototype's prototype, and so on until it reaches `null`.
```mermaid
flowchart LR
A[instance object] --> B[instance prototype]
B --> C[parent prototype]
C --> D[Object.prototype]
D --> E[null]
```
### Property Lookup Example
```js
const animal = {
eats: true,
speak() {
return "sound";
}
};
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.barks); // own property
console.log(dog.eats); // found on prototype
console.log(dog.speak());
```
### Why Prototypes Exist
They allow many objects to share behavior without copying methods onto every instance. That saves memory and provides a natural form of delegation.
If one shared method lives on the prototype, every instance can use it through lookup.
### Constructor Functions and Prototypes
Before `class` syntax, constructor functions were the common way to create related objects.
```js
function User(name) {
this.name = name;
}
User.prototype.describe = function describe() {
return `User: ${this.name}`;
};
const u1 = new User("Isha");
console.log(u1.describe());
```
Every `User` instance can access `describe` via the prototype chain.
### `class` Syntax: Cleaner Surface, Same Underlying Model
```js
class AdminUser extends User {
deletePost() {
return "deleted";
}
}
```
`class` makes the code look more classically object-oriented, but under the hood JavaScript is still using prototypes.
This is important in interviews. A good answer is:
"JavaScript classes are primarily syntactic sugar over prototype-based inheritance, with some stricter rules and cleaner syntax."
### Prototype Delegation vs Classical Inheritance
In class-based languages, people often imagine copying a parent blueprint into a child blueprint. JavaScript is closer to delegation: if an object does not have a property, it asks another object up the chain.
That is a simpler and more accurate mental model.
## Functions Are Objects Too
JavaScript functions are special objects.
That means functions can:
- be passed around as values
- have properties
- be stored in arrays or objects
- act as constructors when used with `new`
This is why JavaScript can support such a strong functional style while still allowing object-oriented patterns. It is also why callbacks, middleware, hooks, and higher-order utilities are so common across web frameworks.
## Event-Driven Thinking: The First Shift Toward Browser Programming
Even before discussing browsers in detail, it helps to understand the event-driven mindset.
Traditional step-by-step programs often look like this:
1. read input
2. process it
3. print output
4. exit
Browser applications are different. They stay alive and wait for things to happen:
- a user clicks a button
- a timer fires
- a network response arrives
- a Promise settles
- the browser asks to repaint
That means much of frontend programming is about registering behavior now that will run later.
```mermaid
flowchart LR
A[App starts] --> B[Register handlers]
B --> C[Wait for events]
C --> D[User click]
C --> E[Timer completes]
C --> F[Network response]
D --> G[Run callback]
E --> G
F --> G
G --> C
```
### Why This Changes How You Design Code
When code runs later instead of immediately, several language features become central:
- closures, because callbacks need access to earlier state
- the call stack, because callbacks run only when the current stack is clear
- `this`, because callback invocation style affects binding
- immutability and state discipline, because time gaps make mutation harder to reason about
This is why frontend interviews repeatedly return to fundamentals. The browser multiplies the importance of small language details.
## Common Misconceptions That Hurt Engineers
### "Hoisting means code moves"
No. Bindings are registered before execution. That is more accurate and leads to better reasoning.
### "Closures copy values"
Not usually. Closures keep access to bindings in an outer lexical environment. That distinction matters when the closed-over value can still change.
### "`this` points to the function's object"
Functions do not have a built-in permanent `this`. For normal functions, call site determines it.
### "Classes replaced prototypes"
No. Classes mostly provide nicer syntax over the prototype system.
### "JavaScript is asynchronous"
JavaScript itself executes synchronous code on a call stack. Async behavior comes from the host environment and scheduling model, which the next chapters explain in detail.
## Interview-Ready Summary
If you need a compact but strong summary, keep these statements ready:
- JavaScript executes code through execution contexts, which store scope, `this`, and runtime state.
- Synchronous JavaScript runs on a call stack, so long-running code blocks progress on the main thread.
- Hoisting is best understood as early binding registration during scope creation, not literal line movement.
- JavaScript uses lexical scope, so variable lookup depends on where code is written.
- Closures allow functions to retain access to outer bindings and are fundamental to callbacks, modules, and UI frameworks.
- For normal functions, `this` depends on call site; arrow functions capture `this` lexically.
- JavaScript object inheritance works through prototype delegation, and `class` syntax is built on top of that model.
## What to Read Next
Continue with [02-javascript-in-the-browser.md](./02-javascript-in-the-browser.md). That chapter answers the next obvious question: if the language gives you functions, objects, and promises, where do timers, the DOM, `fetch`, and events actually come from inside a browser?