Building in PublicRead Blog
JavaScript Core Concepts

JavaScript Core Concepts

D
Dev.Log
30 min read
4 views
Share:

Execution Context & Call Stack

An Execution Context is the environment in which JavaScript code is evaluated and executed. A new execution context is created each time a function is called.

Types of Execution Contexts

  • Global Execution Context (GEC): Created once when the script starts. Sets up the global object (window/global) and this.
  • Function Execution Context (FEC): Created every time a function is invoked. Has its own Variable Environment, Scope Chain, and this binding.
  • Eval Execution Context: Created inside eval()—avoid in production.

Two Phases of Execution Context

PhaseWhat Happens
Creation PhaseMemory is allocated.vardeclarations are hoisted toundefined. Function declarations are fully hoisted.thisis set.
Execution PhaseCode runs line by line. Variables are assigned. Functions are called.

The Call Stack

The Call Stack is a LIFO (Last In, First Out) data structure that tracks execution contexts. When a function is called, it's pushed onto the stack; when it returns, it's popped off. So, call stack maintains the order of execution of execution contexts.

Data Types

CategoryTypes
PrimitiveString, Number, Boolean, Symbol, Null, Undefined
Non-primitiveObject, Array, Date

Hoisting (var vs let/const, TDZ)

Hoisting is JavaScript's mechanism of moving declarations to the top of their scope during the Creation Phase. Only declarations are hoisted, NOT initializations.

KeywordHoisting Behavior
varHoisted and initialized to undefined. Can be accessed before declaration (gives undefined, not error).
letHoisted but NOT initialized. Accessing before declaration → ReferenceError (TDZ).
constSame as let. Also requires initialization at declaration.
function declarationFully hoisted — both name and body. Can call before defining.
function expression / arrow fnTreated like var/let/const — only variable declaration hoisted, not the function body.

Temporal Dead Zone (TDZ)

The Temporal Dead Zone is the period between when a variable declared with let or const is hoisted and when it's initialized. During this time, the variable exists but cannot be accessed. Attempting to access it throws a ReferenceError.

Example: console.log(x); // ReferenceError let x = 5;

Q: Why does var have function scope but let/const have block scope?


It's a language design choice. var was the original keyword and scopes to the nearest function (or global). let and const were introduced in ES6 to prevent common bugs—they scope to the nearest {} block, making them safer in loops and conditionals.

Closures & Lexical Scope

A closure is a function that remembers variables from its lexical scope (where it was created) even when executed outside that scope.

Lexical Scope

JavaScript uses lexical (static) scoping—a variable's accessibility is determined by where it's written in the code, not where it's called from.

Classic Closure Example

function makeCounter() { let count = 0; return { increment: () => ++count, decrement: () => --count, value: () => count }; } const c = makeCounter(); c.increment(); c.increment(); console.log(c.value()); // 2 ← 'count' is kept alive by closure


Common Closure Pitfall—var in Loops

Pitfall & Fix

// BUG: prints 5, 5, 5, 5, 5 for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); } // FIX 1: use let (block-scoped per iteration) for (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); } // FIX 2: IIFE to capture i for (var i = 0; i < 5; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); }


React & Closures—Stale Closure

Stale Closure in React Hooks

useEffect(() => { const id = setInterval(() => console.log(count), 1000); // 'count' is stale! return () => clearInterval(id); }, []); // empty deps — closure captures initial count (0) // Fix: add count to dependency array }, [count]);

Q: What is the difference between a closure and a callback?


A callback is a function passed as an argument to be called later. A closure is any function that captures variables from its outer scope. A callback can be a closure (and often is), but not all closures are callbacks.

Q: How do closures help with data privacy?


Variables declared in the outer function aren't accessible from outside. Only the returned functions (closures) can read or modify them. This is the module pattern—used before ES modules.

Scope Chain & Shadowing

When JS can't find a variable in the current scope, it walks up the scope chain — the series of nested scopes — until it hits the global scope. If still not found: ReferenceError.

Shadowing

When a variable in an inner scope shares the same name as one in an outer scope, the inner declaration 'shadows' the outer one within that block.

Shadowing Example

let x = 'global'; function outer() {  let x = 'outer';    // shadows global x  function inner() {    let x = 'inner';  // shadows outer x    console.log(x);   // 'inner'  }  inner();  console.log(x);     // 'outer' } console.log(x);       // 'global'

Q: Can var shadow a let/const variable in an outer scope?


Yes. var declarations in a function create a new scope and shadow outer variables. However, you cannot redeclare a let/const inside the same block scope using var — it throws a SyntaxError.

Event Loop

JavaScript is single-threaded. The Event Loop is the mechanism that allows async operations to work without blocking the main thread.

ComponentDescription
Call StackExecutes synchronous code. LIFO structure.
Web APIsBrowser/Node APIs (setTimeout, fetch, DOM events) — offload async work here.
Microtask QueueHolds resolved Promises, queueMicrotask callbacks. Drained FULLY before any macrotask.
Macrotask QueueHolds setTimeout, setInterval, I/O callbacks. One task per event loop tick.
Event LoopChecks: if Call Stack empty → drain Microtasks → run next Macrotask → repeat.

Execution Order Example

console.log('1');                              // sync setTimeout(() => console.log('2'), 0);         // macrotask Promise.resolve().then(() => console.log('3'));// microtask console.log('4');                              // sync Output: 1, 4, 3, 2 Reason: sync runs first → microtask queue → macrotask queue

Q: What happens if you have an infinite loop in a Promise chain?


The microtask queue will never drain, starving the macrotask queue. The browser tab will hang because no user events (clicks, renders) can be processed — they're all macrotasks.

Microtask Queue vs Macrotask Queue (Callback Queue)

Microtask QueueMacrotask Queue (Callback Queue)
Promise .then/.catch/.finallysetTimeout / setInterval
queueMicrotask()setImmediate (Node.js)
MutationObserver callbacksI/O callbacks (Node.js)
async/await continuationsUI rendering events, requestAnimationFrame
Runs completely after each taskOne task per event loop iteration
Higher priorityLower priority

Q: Why do Promises run before setTimeout(fn, 0)?


setTimeout(fn, 0) places fn in the macrotask queue. Promise callbacks are microtasks. After synchronous code finishes, the event loop drains the entire microtask queue before picking the next macrotask. This is why Promises resolve first.

Promises, async/await, Promise.all/allSettled/race/any

A Promise represents the eventual completion (or failure) of an async operation. States: pending → fulfilled | rejected.

Promise Combinators

MethodBehavior & When to Use
Promise.all(arr)Resolves when all resolve. Rejects immediately if any reject. Use when you need all results and one failure should abort.
Promise.allSettled(arr)Waits for all to settle (fulfill or reject). Returns array of {status, value/reason}. Use when you need every outcome regardless of failures.
Promise.race(arr)Resolves/rejects with the first settled promise. Use for timeouts or fastest-wins scenarios.
Promise.any(arr)Resolves with the first fulfilled promise. Rejects only if all reject (AggregateError). Use when you want the first success.

async/await Pattern

async function fetchUser(id) { try { const res = await fetch(/api/users/${id}); if (!res.ok) throw new Error(HTTP ${res.status}); const data = await res.json(); return data; } catch (err) { console.error('Fetch failed:', err); throw err; // re-throw if caller needs to handle } }

Q: What is the difference between Promise.all and Promise.allSettled?


Promise.all is fail-fast — one rejection rejects the whole batch. Promise.allSettled always waits for everything and returns the outcome of each. In React, use allSettled when making multiple independent API calls (e.g., loading dashboard widgets) so one failure doesn't blank the entire screen.

Q: How does async/await differ from .then() chains?


They compile to the same thing under the hood — async/await is syntactic sugar over Promises. async/await reads like synchronous code and makes error handling with try/catch easier, while .then chains can become callback-like in complex flows.

Prototypes & Prototypal Inheritance

Every JavaScript object has an internal [[Prototype]] link. When a property or method isn't found on the object, JavaScript walks up the prototype chain until it finds it or reaches null.

Prototype Chain

function Animal(name) { this.name = name; } Animal.prototype.speak = function() { return this.name + ' makes a noise.'; }; function Dog(name) { Animal.call(this, name); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { return 'Woof!'; }; const d = new Dog('Rex'); d.speak(); // 'Rex makes a noise.' — inherited from Animal


ES6 Classes are syntactic sugar over prototypal inheritance. Under the hood, class methods are added to the prototype.

Q: What is the difference between proto and prototype?


proto (or [[Prototype]]) is the actual link on an object instance pointing to its prototype. prototype is a property on constructor functions that becomes the proto of instances created with 'new'. For example, Dog.prototype === new Dog().proto

Q: What does Object.create(null) do?


Creates an object with no prototype chain — a truly plain dictionary with no inherited methods like toString or hasOwnProperty. Useful for creating lookup maps that won't have unexpected property collisions.

'this' Keyword (Default, Implicit, Explicit, Arrow)

ContextValue of 'this'
Global (non-strict)window (browser) / global (Node)
Global (strict mode)undefined
Regular function callGlobal object or undefined (strict)
Method call obj.fn()obj — the object before the dot
new Constructor()The newly created instance
Arrow functionInherited from enclosing lexical scope — never has its own 'this'
call/apply/bindExplicitly set to the first argument
Event handler (regular fn)The DOM element that fired the event
Event handler (arrow fn)Whatever 'this' was in the surrounding scope

Arrow Function 'this' in React

class Timer extends React.Component { tick() { console.log(this); } // 'this' = undefined if passed as callback tickArrow = () => { console.log(this); } // 'this' = component instance ✓ render() { // WRONG: this.tick loses context when passed as a ref return <button onClick={this.tick}>X</button>; // CORRECT options: // 1. Arrow class field: onClick={this.tickArrow} // 2. Bind in constructor: this.tick = this.tick.bind(this) // 3. Inline arrow: onClick={() => this.tick()} } }

call, apply, bind

MethodDifference
fn.call(ctx, a, b, c)Invokes fn immediately. Args passed individually.
fn.apply(ctx, [a, b, c])Invokes fn immediately. Args passed as an array.
fn.bind(ctx, a, b)Returns a new function with 'this' permanently bound. Supports partial application.

Practical Examples

const user = { name: 'Alice' }; function greet(greeting, punct) { return ${greeting}, ${this.name}${punct}; } greet.call(user, 'Hello', '!'); // 'Hello, Alice!' greet.apply(user, ['Hi', '?']); // 'Hi, Alice?' const sayHi = greet.bind(user, 'Hey'); // partial sayHi('.'); // 'Hey, Alice.'