THN Interview Prep

JS Fundamentals: SDE-3 Reference

This guide covers the core foundational concepts of JavaScript, detailing memory management, runtime behavior, and language specifications.


1. Call Stack

The Call Stack is a LIFO (Last In, First Out) data structure managed by the JavaScript engine (e.g., V8) to track function execution. It holds Execution Contexts (activation records) representing the active subroutines of the application.

Loading diagram…

Execution Contexts & Stack Frames

Every time a function is invoked, a new Execution Context is created and pushed onto the Call Stack. Each frame contains:

  1. Local Variables: Primitive values and pointers to objects in the heap.
  2. Parameters: Function arguments passed by the caller.
  3. Return Address: Instruction pointer to return to once execution completes.
  4. Scope Chain Link: Reference to outer Lexical Environments.
  5. this Binding: The runtime evaluation context.

Stack Overflow Limits

Because stack memory is fixed and allocated per thread, infinite recursion consumes all available space, leading to a stack overflow error (RangeError: Maximum call stack size exceeded). In modern engines, this limit is generally between 10,000 and 50,000 frames depending on the payload size per frame.

// Demonstrating Stack Depth Limit
let depth = 0;
function recurse() {
  depth++;
  recurse();
}

try {
  recurse();
} catch (e) {
  console.log(`Max stack depth: ${depth}`); // e.g., ~11420 in Node.js
  console.error(e.stack.split('\n').slice(0, 3).join('\n'));
}

Tail Call Optimization (TCO)

The ECMAScript 6 (ES6) specification mandates Proper Tail Calls (PTC). Under PTC, if a function call is in the tail position (the final action before returning), the engine can overwrite the current stack frame instead of allocating a new one, keeping stack space complexity at $O(1)$.

// Tail-Recursive Factorial (Optimized under strict ES6 PTC)
"use strict";
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc); // Tail call: function call is the return expression
}

[!WARNING] While defined in the ES6 spec, Safari (JavaScriptCore) is currently the only major engine supporting TCO. V8 (Chrome/Node.js) and SpiderMonkey (Firefox) removed tail call optimization due to difficulties in recovering accurate stack traces during debugging and error tracking.


2. Primitive Types

JavaScript has 7 primitive types: string, number, bigint, boolean, undefined, null, and symbol.

TypeSize / LimitsMemory LocationDescription
undefined$0$ bitsStack (Tagged Pointer)Automatically assigned to uninitialized variables.
null$0$ bitsStack (Tagged Pointer)Represents intentional absence of object reference.
boolean$1$ bit representationStack (Tagged Pointer)true or false.
number64-bit Double Precision FloatHeap or Stack (tagged)IEEE 754 float. Safe integers up to $2^53 - 1$.
bigintArbitrary PrecisionHeap (HeapObject)Handles integers exceeding the safe IEEE 754 limit.
stringUTF-16 code unitsHeap (Immutable Flat/Cons)Sequences of characters, stored as immutable objects.
symbolGlobally UniqueHeap (HeapObject)Guaranteed unique identifiers used as object keys.

SMI (Small Integer) vs HeapObject Tagging in V8

To avoid allocating primitives on the heap (which adds garbage collection overhead), V8 uses Pointer Tagging. On 64-bit architectures, V8 uses a process called pointer compression, representing variables as 32-bit values:

  • SMI (Small Integer): For numbers within a 31-bit signed range ($-2^30$ to $2^30 - 1$), V8 stores the number directly in the pointer itself. The least significant bit is set to 0 to signal that it is an SMI, bypassing heap allocation.
  • HeapObject: If the value is a string, object, or a large number (double precision float), the pointer ends with 1, signaling it points to an address in the heap.
Loading diagram…

Autoboxing (Wrapper Objects)

Primitives do not have methods, yet we write "hello".toUpperCase(). When a method is called on a primitive, JavaScript dynamically creates a temporary wrapper object (e.g., new String("hello")), runs the method, and immediately discards the wrapper, making it eligible for garbage collection.

const name = "Antigravity";
name.customProp = "Senior JS"; // Autoboxed wrapper created, property assigned, wrapper garbage collected
console.log(name.customProp);  // undefined (new wrapper created, property checked, not found)

3. Value Types and Reference Types

JavaScript enforces a strict distinction between how data types are allocated in memory and passed.

Loading diagram…

Pass-by-Value vs Call-by-Sharing

In JavaScript, all parameters are passed by value. However, for objects, the "value" passed is the reference pointer. This paradigm is called Call-by-Sharing:

  1. Reassigning a passed object parameter inside a function does not affect the original reference outside the function.
  2. Mutating properties of the passed object does mutate the original object outside.
function modify(primitive, object1, object2) {
  primitive = 999;                        // Reassignment: local copy modified
  object1.name = "Mutated";               // Mutation: shared memory updated
  object2 = { name: "Reassigned Object" }; // Reassignment: local pointer overwritten
}

let num = 42;
let user1 = { name: "Original 1" };
let user2 = { name: "Original 2" };

modify(num, user1, user2);

console.log(num);   // 42
console.log(user1); // { name: "Mutated" }
console.log(user2); // { name: "Original 2" }

4. Type Coercion & Duck Typing

JavaScript is dynamically and weakly typed. It coerces types implicitly when performing operations on mixed types.

Abstract Operations: ToPrimitive, ToNumber, ToString

The ECMAScript specification defines internal algorithms for type conversions:

  1. ToPrimitive(input, PreferredType): Converts an object to a primitive. It calls the object's [Symbol.toPrimitive](hint) method. If absent, it runs valueOf() or toString() depending on the hint ("string", "number", or "default").
  2. ToNumber(input):
    • undefined $\to$ NaN
    • null $\to$ 0
    • true $\to$ 1, false $\to$ 0
    • string $\to$ parsed integer/float, empty string "" $\to$ 0.
  3. ToString(input): Serializes values (e.g. [] $\to$ "", [1, 2] $\to$ "1,2", {} $\to$ "[object Object]").
// Custom Symbol.toPrimitive overrides default valueOf / toString behavior
const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return 100;
    if (hint === "string") return "cool";
    return 42; // default
  }
};

console.log(+obj);     // 100 (hint: number)
console.log(`${obj}`); // "cool" (hint: string)
console.log(obj + 1);  // 43 (hint: default -> 42 + 1)

Coercion Rules & Falsy Values

There are exactly 8 falsy values in JavaScript that coerce to false in boolean contexts: false, 0, -0, 0n (BigInt zero), "" (empty string), null, undefined, and NaN. All other values coerce to true.

Duck Typing & Structural Safety

JavaScript uses Duck Typing ("If it walks like a duck and quacks like a duck, it is a duck"). Runtimes evaluate properties and methods on objects rather than checking explicit type contracts or classes.


5. == vs === vs typeof

The Abstract Equality Comparison Algorithm (==)

The == operator performs implicit type coercion if the operands are of different types.

[!IMPORTANT] Key == Coercion Rules (from ES Spec):

  1. If one is null and the other undefined, they return true.
  2. If one is a string and the other is a number, the string is converted to a number using ToNumber.
  3. If one is a boolean, it is converted to a number (true $\to$ 1, false $\to$ 0) and then compared.
  4. If one is an object and the other a primitive, the object is converted to a primitive via ToPrimitive.
[] == ![]; // true
// Step-by-step resolution:
// 1. ![] evaluates to false -> [] == false
// 2. Boolean converts to number -> [] == 0
// 3. Object (Array) ToPrimitive -> "" == 0
// 4. String converts to number -> 0 == 0 -> true

The Strict Equality Comparison (===)

The === operator performs no coercion. It returns true only if both the type and the value are identical.

  • NaN Exception: NaN === NaN is false. To check for NaN, use Number.isNaN() or Object.is().
  • Signed Zero: -0 === +0 is true.

Object.is()

Introduced in ES6, Object.is(val1, val2) determines if two values are the same value. It behaves like === except:

  1. Object.is(NaN, NaN) returns true.
  2. Object.is(-0, +0) returns false.
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(-0, +0));   // false

The typeof Operator and its Quirks

The typeof operator returns a string indicating the type of the unevaluated operand.

  • typeof null returns "object": This is a legacy bug from the first version of JavaScript. Values were stored in 32-bit units composed of a type tag and the actual value. The object type tag was 000. Since null was represented as the null pointer (0x00 in most platforms), its type tag read as 000, causing typeof to mistakenly return "object".

6. Scope and Closures

Scope determines the accessibility of variables and functions in different parts of code during execution.

Scope Chain & Lexical Environment

JavaScript uses Lexical Scoping (static scoping). The scope structure is resolved at compile/parse time based on where functions are declared, not where they are executed. An Execution Context contains a Lexical Environment, which consists of:

  1. Environment Record: Local variable storage.
  2. Outer Reference: Link to parent Lexical Environment (the Scope Chain).
function outer() {
  const x = 10;
  function inner() {
    console.log(x); // Looks up x via the Outer Reference link -> 10
  }
  return inner;
}
const closureFn = outer();
closureFn();

var vs let vs const

Featurevarletconst
ScopeFunction ScopeBlock Scope ({})Block Scope ({})
HoistingInitialized to undefinedHoisted but uninitialized (TDZ)Hoisted but uninitialized (TDZ)
ReassignableYesYesNo
RedeclarableYesNoNo

Temporal Dead Zone (TDZ)

When execution enters a scope containing let or const declarations, the variables are allocated in memory but remain uninitialized. Any access to these variables before their declaration line throws a ReferenceError. This region of code is the Temporal Dead Zone.

function testTDZ() {
  // TDZ for variable 'a' starts here
  try {
    console.log(a); // ReferenceError
  } catch (e) {
    console.error(e.message);
  }
  let a = 10; // TDZ ends for 'a'
  console.log(a); // 10
}
testTDZ();

Mark this page when you finish learning it.

Spotted something unclear or wrong on this page?

On this page