cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Advanced recursion

Master tail calls, trampolines, CPS, deep clone, and deep equal while keeping recursive JavaScript safe for real nested data.

By the end, you can
  • 01
    Identify tail positionTrace accumulator recursion and explain why portable JavaScript still needs a stack-safe fallback.
  • 02
    Flatten deep recursionUse trampolines or explicit stacks when the input can be tens of thousands of steps deep.
  • 03
    Walk recursive data safelyDeep-clone and deep-compare nested objects, arrays, Dates, Maps, and cycles with honest policies.

Recursion after the basics

The beginner Recursion lesson taught the base case, the recursive case, and why the call stack grows while calls wait. This lesson starts from there. We will not repeat nesting dolls and simple factorial. Instead, we will ask: what if the input is 100,000 levels deep, or the data points back to itself?

Advanced recursion means controlling the shape of recursive work. Sometimes you rewrite the function so the recursive call is the final action. Sometimes you return a thunk and let a loop run it. Sometimes you stop using recursive calls entirely and keep your own stack in an array. For recursive data, you also remember objects you have already seen.

Plain definition

A tail call is a call in the final position of a function branch. A trampoline is a loop that repeatedly runs returned thunks. Continuation-passing style makes “what happens next” an explicit callback. Deep clone and deep equal use recursion over data, plus a WeakMap, so cycles do not trap them forever.

Real-life analogyFolders inside folders

Imagine opening folders inside folders on your computer. If each folder says “open the next one, then come back,” the return list grows. A list can hold the next folders explicitly instead.

In real life: Open a folder inside another folder
In JavaScript: A recursive step reaches a smaller problem
In real life: Keep folder names in a list
In JavaScript: A trampoline returns a thunk, or a loop uses a stack
In real life: Mark folders already opened
In JavaScript: A WeakMap marks objects already cloned or compared
In real life: No folder waits after the next one
In JavaScript: A tail call has no work after the call returns

Where the analogy stops: Folders do not point back to themselves. JavaScript data can, so cycles need a seen map.

You will connect these ideas to stack and heap, closures, WeakMap and WeakSet, and messaging and cloning with structuredClone.

Tail calls: when nothing waits after the call

STEP THROUGH

A call is in tail position when its result is returned directly. Compare the two factorials below. Line 3 in the first function is not tail position because n * still waits for the recursive answer. Line 8 in the accumulator version is tail position because the next call receives the updated product and no multiply remains.

Tail position vs non-tail positionPop out in the code editor (opens in a new tab)JavaScript
function factorial(n) {  if (n <= 1) return 1;  return n * factorial(n - 1);} function factorialTail(n, acc = 1) {  if (n <= 1) return acc;  return factorialTail(n - 1, n * acc);} console.log(factorial(5));console.log(factorialTail(5));
Four words that sound similar
TermMeaningExample
Non-tail recursionThe recursive call is used by more work after it returns.return n * factorial(n - 1); waits to multiply.
Tail recursionThe recursive call is the final action in that branch.return factorialTail(n - 1, n * acc); has no pending multiply.
Proper tail callA spec feature that can reuse a frame for a strict-mode tail call.ES2015 defines it; Safari's JavaScriptCore implements it.
TrampolineA user-land loop that repeatedly calls returned thunks.Portable in V8, SpiderMonkey, and JavaScriptCore.

Step through the accumulator version. Watch acc become 1, then 5, then 20, then 60, then 120. That is the pending multiplication moved into an argument.

Tail position factorial replay
Step 0 of 16Ready
Your turn: follow the blue line

Step through a tail-recursive factorial. Watch acc carry the waiting multiplication so line 3 has no work left after the recursive call.

Running in
  1. script
Next: line 6
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function factorialTail(n, acc = 1) {  if (n <= 1) return acc;  return factorialTail(n - 1, n * acc);} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Proper tail calls are not portable

ES2015 specifies proper tail calls for strict-mode JavaScript, but support is uneven. Current web research and engine behavior agree: Safari’s JavaScriptCore implements proper tail calls; V8, used by Chrome and Node, does not; SpiderMonkey, used by Firefox, does not. So write tail-recursive JavaScript for clarity, not as a cross-browser stack guarantee.

The next snippet deliberately throws in Chrome and Node. It is tail-recursive and strict, but V8 still grows the stack. The exact depth varies, so the example uses a large number and asks only for the error, not for a magic limit.

Deliberate stack overflow in V8Pop out in the code editor (opens in a new tab)JavaScript
"use strict";function dive(n) {  if (n === 0) return "done";  return dive(n - 1);} console.log(dive(100000));

Trampolines: return the next jump instead of taking it

INTERACTIVE

A thunk is a function with no arguments that delays work. A trampoline is a loop that keeps calling thunks until the result is not a function. Instead of nesting sumThunk(5) inside sumThunk(4) inside sumThunk(3), each call returns the next thunk to the loop.

Trampoline replay: thunks become loop turns
Step 0 of 18Ready
Your turn: follow the blue line

A trampoline turns recursive steps into returned thunks. Step through the loop: each turn calls one thunk and then the stack is flat again.

Running in
  1. script
Next: line 16
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function trampoline(task) {  let current = task;  let steps = 0;  while (typeof current === "function") {    current = current();    steps = steps + 1;  }  return { value: current, steps };} function sumThunk(n, acc = 0) {  if (n === 0) return acc;  return () => sumThunk(n - 1, acc + n);} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Read the loop line by line: current starts as a function, line 4 asks whether it is still a function, line 5 calls exactly one thunk, and line 6 counts that turn. When a number appears, line 8 returns it.

Trampoline scale check
100,000-step trampolinePop out in the code editor (opens in a new tab)JavaScript
function trampoline(task) {  let current = task;  let steps = 0;  while (typeof current === "function") {    current = current();    steps = steps + 1;  }  return { value: current, steps };} function sumThunk(n, acc = 0) {  if (n === 0) return acc;  return () => sumThunk(n - 1, acc + n);} const result = trampoline(() => sumThunk(100000));console.log(result.value);console.log(result.steps > 100000);
Output100,000 input
value5,000,050,000
steps > inputtrue
Try it yourself
Choose how many recursive steps to flatten

The displayed source changes only the number. The work still runs as a loop of thunks.

The trampoline finished 100,001 loop turns and returned 5,000,050,000 without asking JavaScript for 100,000 nested stack frames.

The value is the arithmetic sum from n down to 1. Large inputs stay fast because each thunk returns before the next one runs.
Why not always trampoline?

A trampoline allocates many small functions and hides the natural stack trace. Use it when you want a recursive functional shape and the depth can be large. For straightforward tree walking, an explicit stack is usually simpler and faster.

Convert recursion to an explicit stack

COMPARE

The safest production rewrite is often not clever. Put pending work in an array, then loop until that array is empty. The heap holds the array; the JavaScript call stack stays one frame deep. This is exactly what you want for a file tree, a DOM walk, a sitemap import, or user-generated comment threads with unknown depth.

Explicit stack: same tree, no recursive calls
Iterative file-tree walkPop out in the code editor (opens in a new tab)JavaScript
const tree = {  name: "site",  children: [    { name: "index.html" },    { name: "assets", children: [      { name: "logo.svg" },      { name: "app.js" },    ] },    { name: "notes.txt" },  ],}; function countFilesIterative(root) {  const stack = [root];  let files = 0;  while (stack.length > 0) {    const node = stack.pop();    if (!node.children) {      files = files + 1;      continue;    }    for (const child of node.children) {      stack.push(child);    }  }  return files;} console.log(countFilesIterative(tree));
Outputfiles

4

Try it yourself
Loop-owned stack

The array named stack is ordinary heap data. The loop pushes children onto it and pops one node at a time, so the JavaScript call stack stays flat.

This is the production fallback when the input can be deeper than the engine's call stack.
Which stack-safety tool is this?
  • `return factorialTail(n - 1, n * acc);`
  • `return () => sumThunk(n - 1, acc + n);`
  • `const stack = [root]; while (stack.length) ...`
  • `sumCps(n - 1, (small) => done(small + n))`
  • Walk 80,000 DOM-like nodes in a CMS import
  • Build a parser where each step returns the next suspended step
Try it yourself
0 of 6 correct

Sort each snippet or scenario by the strategy it uses. The answers explain the trade-off.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Continuation-passing style: pass “what to do next”

CONCEPT

In continuation-passing style, a function does not return its final answer to its caller in the usual way. It receives a callback, often named done or cont, and calls that callback with the answer. The callback is the continuation: the rest of the program written as a value.

Continuation-passing sumPop out in the code editor (opens in a new tab)JavaScript
function sumCps(n, done) {  if (n === 0) return done(0);  return sumCps(n - 1, (smaller) => {    return done(smaller + n);  });} sumCps(4, (answer) => console.log(answer));

Line 2 says the base case should call done(0). Line 3 passes a new continuation to the smaller problem. That continuation says: when the smaller sum arrives, add the current n, then call the older continuation. This is the same mental model behind callbacks and later async code: give another piece of code the next thing to do.

CPS is not automatically stack-safe

This direct CPS example still calls sumCps recursively, so a huge n can overflow in V8. CPS becomes stack-safe when paired with a trampoline or when the continuation is resumed asynchronously by the host.

Recursive data: deep clone with a WeakMap

INTERACTIVE

Recursive data contains smaller values with the same shape. An object can contain an array of objects; a Map can point to objects; a comment can contain replies that contain replies. The challenge is cycles: profile.self = profile points back to the object you started from.

A safe deep clone handles primitives directly, creates the destination container early, stores the source-to-copy pair in a WeakMap, then clones children. The early store is what makes cycles work.

Deep clone: custom helper vs structuredClone
Deep clone with WeakMapPop out in the code editor (opens in a new tab)JavaScript
function deepClone(value, seen = new WeakMap()) {  if (Object(value) !== value || typeof value === "function") return value;  if (seen.has(value)) return seen.get(value);  if (value instanceof Date) return new Date(value.getTime());  if (value instanceof Map) {    const clone = new Map();    seen.set(value, clone);    for (const [key, entry] of value) {      clone.set(deepClone(key, seen), deepClone(entry, seen));    }    return clone;  }  if (Array.isArray(value)) {    const clone = [];    seen.set(value, clone);    for (const item of value) clone.push(deepClone(item, seen));    return clone;  }  const clone = Object.create(Object.getPrototypeOf(value));  seen.set(value, clone);  for (const key of Reflect.ownKeys(value)) {    clone[key] = deepClone(value[key], seen);  }  return clone;} const profile = {  name: "Ada",  dates: [new Date("2020-01-02T00:00:00Z")],  meta: new Map([["visits", 3]]),};profile.self = profile; const copy = deepClone(profile);console.log(copy !== profile);console.log(copy.dates[0] instanceof Date);console.log(copy.meta.get("visits"));console.log(copy.self === copy);
Outputcustom
  1. true
  2. true
  3. 3
  4. true
Try it yourself

The custom clone preserves Dates, Maps, arrays, plain object links, and the cycle by remembering source objects in a WeakMap.

Both examples use real JavaScript. The custom helper's policy is to keep function references rather than cloning behavior.
Deep clone choices and limits
ValueCustom clonestructuredClone honesty
Nested arrays and objectsCustom clone can copy recursively; structuredClone handles plain structured data too.Use a seen map for cycles.
DateCopy with new Date(value.getTime()) or let structuredClone handle it.Do not turn it into a string unless that is your API.
MapClone keys and values recursively after placing the new Map in seen.Map insertion order is part of its data.
FunctionsA custom helper may keep the same reference by policy.structuredClone throws DataCloneError.
DOM nodesUse DOM APIs such as node.cloneNode(true) instead.structuredClone does not clone live DOM nodes.
Class prototypesA custom helper can preserve prototypes; descriptors still need extra work.structuredClone returns ordinary data, not your class methods.

Prefer structuredClone for plain structured data that the platform supports: arrays, objects, Dates, Maps, Sets, ArrayBuffers, and cycles. Reach for a custom helper when you need a specific policy for functions, prototypes, descriptors, or values the structured clone algorithm rejects. DOM nodes are live host objects; clone them with DOM APIs, not this data helper.

Deep equal: equality is a policy

INTERACTIVE

A deep equal helper walks two values together. It needs to answer policy questions before it touches recursion: should NaN equal NaN? Should -0 equal 0? Does object key order matter? Are arrays ordered? What happens when both sides point back to themselves?

Deep equal policy lab
Deep equal with pair trackingPop out in the code editor (opens in a new tab)JavaScript
function deepEqual(a, b, seen = new WeakMap()) {  if (Object.is(a, b)) return true;  if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;  const known = seen.get(a);  if (known?.has(b)) return true;  if (!known) seen.set(a, new WeakSet([b]));  else known.add(b);  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;  if (a instanceof Date) return Object.is(a.getTime(), b.getTime());  if (Array.isArray(a)) {    if (!Array.isArray(b) || a.length !== b.length) return false;    return a.every((item, index) => deepEqual(item, b[index], seen));  }  if (a instanceof Map) {    if (!(b instanceof Map) || a.size !== b.size) return false;    const aEntries = [...a.entries()];    const bEntries = [...b.entries()];    return aEntries.every(([key, value], index) => {      return deepEqual(key, bEntries[index][0], seen) && deepEqual(value, bEntries[index][1], seen);    });  }  const aKeys = Reflect.ownKeys(a);  const bKeys = Reflect.ownKeys(b);  if (aKeys.length !== bKeys.length) return false;  const bKeySet = new Set(bKeys);  for (const key of aKeys) {    if (!bKeySet.has(key) || !deepEqual(a[key], b[key], seen)) return false;  }  return true;} const left = { a: NaN, b: -0, nested: [1, { x: 2 }] };const right = { nested: [1, { x: 2 }], b: 0, a: NaN };console.log(deepEqual(left, right));right.b = -0;console.log(deepEqual(left, right));left.self = left;right.self = right;console.log(deepEqual(left, right));console.log(deepEqual([1, 2], [2, 1]));
Selected factsminus-zero
first comparisonfalse
after right.b = -0true
Try it yourself

This helper uses Object.is, so NaN equals NaN, but -0 and 0 are different by policy.

A deep equal helper is a policy, not a universal truth. State the policy in tests so future changes are deliberate.

The helper in this lesson uses Object.is, so NaN matches itself and -0 remains different from 0. Object property order does not matter: keys are matched by identity and name. Array order does matter because index position is part of the value. Cycles are handled by a WeakMap from left objects to a WeakSet of right objects already compared with them.

Where you’ll use advanced recursion

These techniques show up whenever front-end code walks unknown nested data:

  • Walking a file tree from a drag-and-drop upload before showing totals.
  • Traversing a DOM-like tree in tests or a content editor without trusting its depth.
  • Rendering comment threads where replies can contain more replies.
  • Normalizing deeply nested JSON returned by an API.
  • Cloning message payloads before sending them to workers or iframes.
  • Comparing form snapshots in state-management tools without false positives from key order.
Choosing the practical tool
QuestionPreferReason
Tail callSource-code position: a call is the last action in a branch.Helps you reason, but does not save stack in most engines.
TrampolineReturn a thunk for the next step; a loop runs the thunks.Good for deep functional pipelines where you want to keep recursive shape.
Explicit stackStore pending nodes in an array and loop.Best production default for huge trees, DOM walks, and file-like data.
CPSPass a continuation callback that represents what should happen next.Great mental bridge to callbacks and async flow; not stack-safe by itself.
Professional habit

If the depth comes from users, files, CMS content, DOM shape, or network data, assume it can be deeper than your test fixture. Use a trampoline or explicit stack, and add a test for a large depth.

Common misconceptions

“Tail-recursive JavaScript is automatically safe.”

Not in Chrome, Node, or Firefox. Tail position is useful, but most engines still grow the stack for ordinary recursive calls.

“A trampoline is just a faster recursive function.”

A trampoline trades call frames for loop turns and function allocations. It is stack-safe, not automatically faster.

“CPS means asynchronous.”

CPS uses callbacks as continuations. The callback can run synchronously, like the example, or later, like browser events and promises.

“structuredClone replaces every custom deep clone.”

It is excellent, but it rejects functions and DOM nodes and does not preserve custom class behavior. Custom clone code is a policy decision.

“Deep equal has one correct answer.”

Teams choose policies for -0, prototypes, Maps, Sets, sparse arrays, descriptors, and cycles. Tests should state those choices.

Mistakes that look reasonable
MisreadBetter readingSafer action
Tail call means optimizedTail call is source position; optimization is engine support.Assume no PTC outside Safari.
Returning a thunk is the resultA thunk is one suspended step.Run it with a trampoline loop.
WeakMap is only for performanceIt preserves object identity and breaks cycles.Store before cloning or comparing children.
Object and array order are the sameObject key order is usually not semantic; array index order is.Document your equality policy.

Practice advanced recursion

5 EXERCISES
Exercise 1 · Warm-upPredict the accumulator factorial

Read the tail-recursive factorial and type the printed number.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function factorialTail(n, acc = 1) {
  if (n <= 1) return acc;
  return factorialTail(n - 1, n * acc);
}
console.log(factorialTail(5));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeProve a trampoline handles 100,000+ turns

    Predict the boolean printed by the proof snippet.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function trampoline(task) {
      let current = task;
      let steps = 0;
      while (typeof current === "function") {
        current = current();
        steps = steps + 1;
      }
      return { value: current, steps };
    }
    
    function sumThunk(n, acc = 0) {
      if (n === 0) return acc;
      return () => sumThunk(n - 1, acc + n);
    }
    
    const result = trampoline(() => sumThunk(100000));
    console.log(result.steps > 100000);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the cycle bug in a deep clone

      What should the fixed cycle check print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function deepClone(value) {
        if (Object(value) !== value) return value;
        const clone = Array.isArray(value) ? [] : {};
        for (const key of Reflect.ownKeys(value)) {
          clone[key] = deepClone(value[key]);
        }
        return clone;
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeName the deep-equal policy

        Predict the two booleans printed by the policy check.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function deepEqual(a, b, seen = new WeakMap()) {
          if (Object.is(a, b)) return true;
          if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
          const known = seen.get(a);
          if (known?.has(b)) return true;
          if (!known) seen.set(a, new WeakSet([b]));
          else known.add(b);
          if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
          if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
          if (Array.isArray(a)) {
            if (!Array.isArray(b) || a.length !== b.length) return false;
            return a.every((item, index) => deepEqual(item, b[index], seen));
          }
          if (a instanceof Map) {
            if (!(b instanceof Map) || a.size !== b.size) return false;
            const aEntries = [...a.entries()];
            const bEntries = [...b.entries()];
            return aEntries.every(([key, value], index) => {
              return deepEqual(key, bEntries[index][0], seen) && deepEqual(value, bEntries[index][1], seen);
            });
          }
          const aKeys = Reflect.ownKeys(a);
          const bKeys = Reflect.ownKeys(b);
          if (aKeys.length !== bKeys.length) return false;
          const bKeySet = new Set(bKeys);
          for (const key of aKeys) {
            if (!bKeySet.has(key) || !deepEqual(a[key], b[key], seen)) return false;
          }
          return true;
        }
        
        console.log(deepEqual(NaN, NaN) + "," + deepEqual(-0, 0));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose a stack-safe site strategy

          A social site imports comment threads from users. Some threads are tens of thousands of replies deep. Which technique should the renderer use first: tail recursion, trampoline, CPS, or an explicit stack?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which call is in tail position?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the tail and non-tail pair print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function nonTail(n) {
                if (n === 0) return 0;
                return 1 + nonTail(n - 1);
              }
              function tail(n, acc = 0) {
                if (n === 0) return acc;
                return tail(n - 1, acc + 1);
              }
              console.log(nonTail(3), tail(3));

              Choose an answer to see the explanation.

            3. Question 3 of 8What is the honest proper-tail-call status in 2026?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does the small trampoline print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function trampoline(task) {
                let current = task;
                let steps = 0;
                while (typeof current === "function") {
                  current = current();
                  steps = steps + 1;
                }
                return { value: current, steps };
              }
              
              function sumThunk(n, acc = 0) {
                if (n === 0) return acc;
                return () => sumThunk(n - 1, acc + n);
              }
              
              console.log(trampoline(() => sumThunk(3)).value);

              Choose an answer to see the explanation.

            5. Question 5 of 8What does the CPS example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function sumCps(n, done) {
                if (n === 0) return done(0);
                return sumCps(n - 1, (smaller) => {
                  return done(smaller + n);
                });
              }
              
              sumCps(4, (answer) => console.log(answer));

              Choose an answer to see the explanation.

            6. Question 6 of 8What does the cycle-aware clone check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function deepClone(value, seen = new WeakMap()) {
                if (Object(value) !== value || typeof value === "function") return value;
                if (seen.has(value)) return seen.get(value);
                if (value instanceof Date) return new Date(value.getTime());
                if (value instanceof Map) {
                  const clone = new Map();
                  seen.set(value, clone);
                  for (const [key, entry] of value) {
                    clone.set(deepClone(key, seen), deepClone(entry, seen));
                  }
                  return clone;
                }
                if (Array.isArray(value)) {
                  const clone = [];
                  seen.set(value, clone);
                  for (const item of value) clone.push(deepClone(item, seen));
                  return clone;
                }
                const clone = Object.create(Object.getPrototypeOf(value));
                seen.set(value, clone);
                for (const key of Reflect.ownKeys(value)) {
                  clone[key] = deepClone(value[key], seen);
                }
                return clone;
              }
              
              const item = { name: "node" };
              item.self = item;
              const copy = deepClone(item);
              console.log(copy !== item && copy.self === copy);

              Choose an answer to see the explanation.

            7. Question 7 of 8What does the deep-equal policy snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function deepEqual(a, b, seen = new WeakMap()) {
                if (Object.is(a, b)) return true;
                if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
                const known = seen.get(a);
                if (known?.has(b)) return true;
                if (!known) seen.set(a, new WeakSet([b]));
                else known.add(b);
                if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
                if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
                if (Array.isArray(a)) {
                  if (!Array.isArray(b) || a.length !== b.length) return false;
                  return a.every((item, index) => deepEqual(item, b[index], seen));
                }
                if (a instanceof Map) {
                  if (!(b instanceof Map) || a.size !== b.size) return false;
                  const aEntries = [...a.entries()];
                  const bEntries = [...b.entries()];
                  return aEntries.every(([key, value], index) => {
                    return deepEqual(key, bEntries[index][0], seen) && deepEqual(value, bEntries[index][1], seen);
                  });
                }
                const aKeys = Reflect.ownKeys(a);
                const bKeys = Reflect.ownKeys(b);
                if (aKeys.length !== bKeys.length) return false;
                const bKeySet = new Set(bKeys);
                for (const key of aKeys) {
                  if (!bKeySet.has(key) || !deepEqual(a[key], b[key], seen)) return false;
                }
                return true;
              }
              
              console.log(deepEqual({ a: NaN }, { a: NaN }));
              console.log(deepEqual(-0, 0));

              Choose an answer to see the explanation.

            8. Question 8 of 8What happens in Chrome or Node when this strict tail-recursive call is 100,000 calls deep?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              "use strict";
              function dive(n) {
                if (n === 0) return "done";
                return dive(n - 1);
              }
              
              console.log(dive(100000));

              Choose an answer to see the explanation.

            Key takeaways

            • Tail position means no work remains after the call, but most JavaScript engines still do not reuse the frame.
            • Trampolines make recursive steps stack-safe by returning thunks and running them in a loop.
            • Explicit stacks are often the clearest production answer for unknown-depth trees.
            • CPS turns “what happens next” into a callback, which connects recursion to callback and async patterns.
            • Deep clone and deep equal need a seen-pair structure, usually a WeakMap, to handle cycles honestly.

            Remember the one-liner.
            Advanced recursion is less about calling yourself and more about deciding where the next step lives: in the call stack, in a thunk, in an explicit array, or in a seen map.

            Up next: Functors and monads, where functional patterns move from recursive control flow to values that map, chain, and carry context.

            CompleteFrontend Clear concepts. Working examples.