Files
Computer-Fundamentals/js/01-javascript-fundamentals.md
T
tarun-elango be31df2d44 more text
2026-04-26 14:09:04 -04:00

22 KiB

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:

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.

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

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.

flowchart BT
    A[third()] --> B[second()]
    B --> C[first()]
    C --> D[global]

Example

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.

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.

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.

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

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.

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.

if (true) {
  const status = "ready";
}

console.log(status); // ReferenceError

var is not block-scoped.

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.

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

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.

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

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

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

function greet() {
  console.log(`Hello ${this.name}`);
}

greet.call({ name: "Lena" });

Constructor call with new

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.

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.

flowchart LR
    A[instance object] --> B[instance prototype]
    B --> C[parent prototype]
    C --> D[Object.prototype]
    D --> E[null]

Property Lookup Example

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.

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

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.

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.

Continue with 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?