cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Classic utilities

Build production-grade deepClone, deepEqual, curry, compose, memoize, once, flattenObject, unflatten, and get utilities with edge-case tests.

By the end, you can
  • 01
    Turn interview prompts into contractsWrite requirements and conformance checks before hardening deepClone, deepEqual, curry, memoize, and path helpers.
  • 02
    Handle the sharp edges deliberatelySupport Dates, RegExps, Maps, Sets, cycles, placeholders, cache clearing, empty containers, and default path values without guessing.
  • 03
    Choose built-ins or libraries honestlyCompare custom utilities with structuredClone, JSON round trips, spread, Lodash, and Ramda so production code stays boring.

Utilities are tiny contracts

A classic utility is a small reusable function with a surprisingly strict promise: clone this graph, compare these values, collect these arguments, cache this result, run this callback once, or turn nested paths into strings and back.

You already met the ideas in advanced recursion, currying, composition, memoization, closures, WeakMap and WeakSet, messaging and cloning, Object.keys, values, entries, and the upcoming polyfill array methods lesson. Here you use them like an interview and utility-library capstone: write the contract, build a naive version, prove the failure, then harden the function.

Definition

Classic utilities are focused helper functions whose behavior is defined by edge-case requirements and conformance tests, not by a vague name like deep or smart.

Real-life analogyA toolbox with safety cards

Picture a shared toolbox. The label helps only if the safety card says what the tool can cut, what it must not touch, and how to inspect the result. A utility function deserves the same treatment.

In real life: Tool label
In JavaScript: Function name such as deepClone
In real life: Safety card
In JavaScript: Requirements and edge-case table
In real life: Right tool for the job
In JavaScript: Built-in, library, or custom helper
In real life: Inspection stamp
In JavaScript: Conformance tests that run real JavaScript

Where the analogy stops: A physical tool does not share nested references, close over variables, or depend on engine-specific clone rules. JavaScript utilities must state those policies explicitly.

Deep clone strategy comparison
CasedeepClonestructuredCloneJSON round-tripSpread
Nested plain dataDeep copyDeep copyDeep copy if JSON-safeNested references stay shared
DateKeeps a DateKeeps a DateBecomes an ISO stringSame Date reference
RegExpKeeps source, flags, and lastIndexKeeps source and flags, resets some state by specBecomes {}Same RegExp reference
Map and SetDeep copies keys and valuesKeeps Map and Set dataBecomes {} or arrays you prepared yourselfSame collection reference
CycleWeakMap rewires to the cloneSupportedThrows TypeErrorOnly the top level is copied
FunctionReturned as the same callableThrows DataCloneErrorDropped or omittedSame function reference
Class instancePrototype kept by default, optional plain objectData copied as a plain objectPlain JSON data onlyPrototype methods are not copied by spread

Clone and compare data

STEP THROUGH

The naive deep-clone interview answer usually handles arrays and plain objects. It fails the moment a graph points back to itself, and it silently loses Dates, RegExps, Maps, Sets, symbols, and prototypes. Mark the failure as deliberate, then fix it.

deepClone requirements become tests
RequirementPolicy
PrimitiveReturn the same value; primitives are already immutable enough.
Arrays and plain objectsAllocate fresh containers and recursively copy their values.
DateCreate new Date(value.getTime()) so identity changes but time stays.
RegExpCopy source, flags, and lastIndex when state matters.
Map and SetAllocate empty collections, register them in seen, then clone entries.
CycleStore source-to-copy pairs in a WeakMap before descending.
Prototype policyPreserve prototypes by default, but allow plain-object output when callers ask.
Naive clone that fails on a cyclePop out in the code editor (opens in a new tab)JavaScript
function cloneJsonStyle(value) {  if (value === null || typeof value !== "object") return value;  if (Array.isArray(value)) return value.map(cloneJsonStyle);  const copy = {};  for (const key of Object.keys(value)) {    copy[key] = cloneJsonStyle(value[key]);  }  return copy;} const profile = { name: "Ada" };profile.self = profile;console.log(cloneJsonStyle(profile));

Line 4 recurses into every property. Line 12 creates the edge case. Line 13 tries to clone the same object again and again until the stack overflows. A production version registers each source object in a WeakMap before walking its children.

Deep clone with a cycle
Step 0 of 16Ready
Your turn: follow the blue line

Step through a real deep clone with a custom prototype, RegExp state, Map, Set, and a cycle. The WeakMap step is the difference between a utility and a demo.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  if (value === null || (typeof value !== "object" && typeof value !== "function")) return value;  if (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 RegExp) {    const copy = new RegExp(value.source, value.flags);    copy.lastIndex = value.lastIndex;    return copy;  }  if (value instanceof Map) {    const copy = new Map();    seen.set(value, copy);    for (const [key, entry] of value) {      copy.set(deepClone(key, options, seen), deepClone(entry, options, seen));    }    return copy;  }  if (value instanceof Set) {    const copy = new Set();    seen.set(value, copy);    for (const entry of value) copy.add(deepClone(entry, options, seen));    return copy;  }  const copy = Array.isArray(value)    ? []    : options.preservePrototype === false      ? {}      : Object.create(Object.getPrototypeOf(value));  seen.set(value, copy);  for (const key of Reflect.ownKeys(value)) {    copy[key] = deepClone(value[key], options, seen);  }  return copy;} class Profile {  constructor(name) { this.name = name; }  greet() { return "Hi " + this.name; }}const original = new Profile("Ada");original.pattern = /clone/gi;original.pattern.lastIndex = 2;original.visits = new Map([[{ day: "Mon" }, new Set([1, 2])]]);original.self = original; const copy = deepClone(original);console.log(copy !== original);console.log(copy instanceof Profile);console.log(copy.pattern.lastIndex);console.log([...copy.visits.values()][0].has(2));console.log(copy.self === copy);
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.
Clone strategy edge-case playground
Clone comparison fragmentJavaScript
const value = makeValue("date");console.log("deepClone:", summarize(deepClone(value)));try { console.log("structuredClone:", summarize(structuredClone(value))); }catch (error) { console.log("structuredClone:", error.name); }try { console.log("JSON:", summarize(JSON.parse(JSON.stringify(value)))); }catch (error) { console.log("JSON:", error.name); }console.log("spread:", summarize({ ...value }));
Observed resultsDate
deepCloneDate:2024-01-02
structuredCloneDate:2024-01-02
JSON round-trip{"when":"2024-01-02T00:00:00.000Z"}
spreadDate:2024-01-02
Try it yourself
Choose the input value

The selected input is Date. Compare what each strategy keeps, loses, or rejects.

The code fragment shows the four strategy calls; the result cards come from the lesson's real helper functions.

structuredClone should be your first stop when it matches the problem: it is built into modern browsers and Node, supports cycles, and understands many platform data types. Write your own clone only when you need a different policy, such as preserving a custom prototype or returning functions by reference.

deepEqual requirements become tests
CasePolicy
NaNEqual, because Object.is(NaN, NaN) is true.
+0 and -0Different in this lesson's policy; switch to === if your product treats them the same.
ArraysBoth sides must be arrays with the same length and same ordered values.
ObjectsPrototypes and own-key counts must match before values recurse.
Dates and RegExpsCompare their internal values, not object identity.
Maps and SetsCompare size and match entries by deep equality, not by object identity alone.
CyclesRemember object pairs so equal circular graphs terminate.
Cycle-safe deepEqualPop 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 || b instanceof Date) return a instanceof Date && b instanceof Date && Object.is(a.getTime(), b.getTime());  if (a instanceof RegExp || b instanceof RegExp) return a instanceof RegExp && b instanceof RegExp && a.source === b.source && a.flags === b.flags && a.lastIndex === b.lastIndex;  if (Array.isArray(a) || Array.isArray(b)) {    return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => deepEqual(item, b[index], seen));  }  if (a instanceof Map || b instanceof Map) {    if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size) return false;    const unmatched = [...b.entries()];    return [...a.entries()].every(([key, value]) => {      const index = unmatched.findIndex(([otherKey, otherValue]) => deepEqual(key, otherKey, seen) && deepEqual(value, otherValue, seen));      if (index === -1) return false;      unmatched.splice(index, 1);      return true;    });  }  if (a instanceof Set || b instanceof Set) {    if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size) return false;    const unmatched = [...b.values()];    return [...a.values()].every((value) => {      const index = unmatched.findIndex((other) => deepEqual(value, other, seen));      if (index === -1) return false;      unmatched.splice(index, 1);      return true;    });  }  const aKeys = Reflect.ownKeys(a);  const bKeys = Reflect.ownKeys(b);  if (aKeys.length !== bKeys.length) return false;  const bKeySet = new Set(bKeys);  return aKeys.every((key) => bKeySet.has(key) && deepEqual(a[key], b[key], seen));} const left = { n: NaN, z: -0, list: [1, { x: 2 }] };const right = { list: [1, { x: 2 }], z: 0, n: NaN };console.log(deepEqual(left, right));right.z = -0;console.log(deepEqual(left, right));left.self = left;right.self = right;console.log(deepEqual(left, right));console.log(deepEqual(new Map([[{ id: 1 }, new Set([2])]]), new Map([[{ id: 1 }, new Set([2])]])));

A naive equality helper starts with a === b and immediately fails NaN. This version states the policy up front with Object.is: NaN equals itself, while +0 and -0 are different. Arrays are ordered, objects must have the same prototype and own-key count, and Maps and Sets match by deep value rather than reference.

Function reshaping utilities

GROUPED ARGS

The currying lesson already explained the mental model. The utility-library version needs a clearer contract: use fn.length as the default arity, accept grouped arguments, and optionally support placeholders. It should not re-explain why currying exists; it should make call sites predictable.

Curry with grouped args and placeholdersPop out in the code editor (opens in a new tab)JavaScript
const __ = Symbol("placeholder");function curry(fn, arity = fn.length) {  return function curried(...args) {    const complete = args.length >= arity && args.slice(0, arity).every((arg) => arg !== __);    if (complete) return fn.apply(this, args.slice(0, arity));    return function collect(...more) {      const merged = args.map((arg) => arg === __ && more.length ? more.shift() : arg).concat(more);      return curried.apply(this, merged);    };  };} const format = (greeting, name, punctuation) => greeting + ", " + name + punctuation;const greet = curry(format);console.log(greet("Hi")("Ada")("!"));console.log(greet("Hi", "Lin")("."));console.log(greet(__, "Grace")("Hello")("!"));

Line 1 creates one placeholder symbol. Line 4 decides whether the first arity slots are filled. Line 7 merges later arguments into earlier placeholders before appending extra grouped arguments. This is still fn.length-based, so default and rest parameters need an explicit arity argument.

Compose, pipe, and async pipePop out in the code editor (opens in a new tab)JavaScript
const compose = (...fns) => (input) =>  fns.reduceRight((value, fn) => fn(value), input);const pipe = (...fns) => (input) =>  fns.reduce((value, fn) => fn(value), input);const pipeAsync = (...fns) => (input) =>  fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(input)); const trim = (text) => text.trim();const lower = (text) => text.toLowerCase();const dash = (text) => text.replaceAll(" ", "-"); console.log(compose(dash, lower, trim)("  Utility Lesson  "));console.log(pipe(trim, lower, dash)("  Utility Lesson  "));pipeAsync(  async (id) => ({ id, role: "admin" }),  async (user) => user.role.toUpperCase(),)(7).then(console.log);

A naive composition helper nests calls by hand. The fixed version turns the function list into a reducer. compose uses reduceRight because the input enters from the right; pipe uses reduce because the input flows left to right. For asynchronous work, start from Promise.resolve(input) and chain with .then(fn).

Caching and one-time work

CACHE POLICY

A naive memoizer serializes arguments with JSON.stringify and hides the cache. That breaks on key order, object identity, functions, and memory cleanup. A production helper takes a resolver argument and exposes the cache so tests and invalidation can be explicit.

memoize with resolver, and once with first resultPop out in the code editor (opens in a new tab)JavaScript
function memoize(fn, resolver = (...args) => JSON.stringify(args)) {  const cache = new Map();  function memoized(...args) {    const key = resolver(...args);    if (cache.has(key)) return cache.get(key);    const result = fn.apply(this, args);    cache.set(key, result);    return result;  }  memoized.cache = cache;  memoized.clear = () => cache.clear();  return memoized;} function once(fn) {  let called = false;  let result;  let error;  let threw = false;  return function runOnce(...args) {    if (!called) {      called = true;      try { result = fn.apply(this, args); }      catch (caught) { threw = true; error = caught; throw caught; }    }    if (threw) throw error;    return result;  };} let calls = 0;const area = memoize((w, h) => { calls++; return w * h; }, (w, h) => w + "x" + h);console.log(area(3, 4));console.log(area(3, 4));console.log(calls);area.clear();console.log(area(3, 4));console.log(calls); const button = { label: "save", init: once(function (suffix) { return this.label + suffix; }) };console.log(button.init("!"));console.log(button.init("?"));

The resolver owns the key policy. Line 10 exposes clear, which is boring but essential in tests. The once helper marks the first call, runs with fn.apply(this, args), and returns the first result forever, even when later calls pass different arguments.

Bonus: debounce and throttle with a fake clockPop out in the code editor (opens in a new tab)JavaScript
function createFakeClock() {  let now = 0;  let nextId = 1;  const tasks = [];  return {    now: () => now,    setTimeout(callback, wait) {      const task = { id: nextId++, time: now + wait, callback, cleared: false };      tasks.push(task);      return task.id;    },    clearTimeout(id) {      for (const task of tasks) if (task.id === id) task.cleared = true;    },    tick(ms) {      const end = now + ms;      while (true) {        const ready = tasks.filter((task) => !task.cleared && task.time <= end).sort((a, b) => a.time - b.time)[0];        if (!ready) break;        ready.cleared = true;        now = ready.time;        ready.callback();      }      now = end;    },  };} function debounce(fn, wait, clock = { setTimeout, clearTimeout }) {  let timeout;  return function debounced(...args) {    clock.clearTimeout(timeout);    timeout = clock.setTimeout(() => fn.apply(this, args), wait);  };} function throttle(fn, wait, clock = { now: Date.now, setTimeout, clearTimeout }) {  let lastRun = -Infinity;  let timeout;  let lastArgs;  let lastThis;  return function throttled(...args) {    const remaining = wait - (clock.now() - lastRun);    lastArgs = args;    lastThis = this;    if (remaining <= 0) {      if (timeout !== undefined) clock.clearTimeout(timeout);      timeout = undefined;      lastRun = clock.now();      fn.apply(lastThis, lastArgs);    } else if (timeout === undefined) {      timeout = clock.setTimeout(() => {        timeout = undefined;        lastRun = clock.now();        fn.apply(lastThis, lastArgs);      }, remaining);    }  };} const log = [];const clock = createFakeClock();const save = debounce((value) => log.push("debounced " + value), 100, clock);save("a");clock.tick(50);save("b");clock.tick(100);const paint = throttle((value) => log.push("throttled " + value), 100, clock);paint("one");clock.tick(20);paint("two");clock.tick(100);console.log(log.join(" | "));

Timer utilities are a bonus, but their tests teach the same habit. debounce waits for quiet before running the latest call. throttle runs at most once per window and keeps the latest trailing arguments. A fake clock proves both without waiting in real time.

Flatten, unflatten, and get

ROUND TRIP

Flattening turns nested data into path keys. The production question is not recursion; it is policy. Are arrays written as indices? What happens to empty objects? Can the inverse rebuild the same shape? What if a real key contains a dot?

Flattening requirements become tests
CasePolicy
Nested objects{ a: { b: 1 } } becomes { 'a.b': 1 }.
ArraysIndices become segments: list.0, list.1, and so on.
Empty objectKeep {} at its path so the inverse does not lose it.
Empty arrayKeep [] at its path for the same reason.
Round tripunflatten(flattenObject(value)) should rebuild JSON-shaped data.
Path helperget(obj, 'a.b[0].c', fallback) returns the fallback only for missing or undefined values.
Flatten and rebuild paths
Step 0 of 11Ready
Your turn: follow the blue line

Step through flattening a nested object into dotted paths, then rebuilding arrays and empty containers on the way back.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const isObject = value !== null && typeof value === "object";  if (!isObject) {    if (prefix) output[prefix] = value;    return output;  }  const entries = Array.isArray(value)    ? value.map((entry, index) => [String(index), entry])    : Object.entries(value);  if (entries.length === 0 && prefix) {    output[prefix] = Array.isArray(value) ? [] : {};    return output;  }  for (const [key, entry] of entries) {    const path = prefix ? prefix + "." + key : key;    flattenObject(entry, path, output);  }  return output;} function unflatten(flat) {  const root = {};  for (const [path, value] of Object.entries(flat)) {    const parts = path.split(".");    let cursor = root;    parts.forEach((part, index) => {      const last = index === parts.length - 1;      if (last) cursor[part] = value;      else cursor = cursor[part] ??= /^\d+$/.test(parts[index + 1]) ? [] : {};    });  }  return root;} const settings = { user: { name: "Ada" }, list: ["js", "utils"], empty: {} };const flat = flattenObject(settings);console.log(flat["user.name"]);console.log(flat["list.0"]);console.log(Array.isArray(flat.empty) ? "array" : typeof flat.empty);console.log(JSON.stringify(unflatten(flat)) === JSON.stringify(settings));
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.
Safe get with dot and bracket pathsPop out in the code editor (opens in a new tab)JavaScript
function get(obj, path, defaultValue) {  const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".").filter(Boolean);  let cursor = obj;  for (const part of parts) {    if (cursor == null) return defaultValue;    cursor = cursor[part];  }  return cursor === undefined ? defaultValue : cursor;} const data = { users: [{ profile: { name: "Ada" } }] };console.log(get(data, "users[0].profile.name", "Unknown"));console.log(get(data, "users[1].profile.name", "Unknown"));

The visible get implementation handles common dot and bracket paths such as users[0].profile.name. In production, arbitrary keys need an escaping policy or an array of path segments; otherwise {"a.b": 1} and {"a": {"b": 1}} look identical to the parser.

When to build or import

SORT IT

Real projects do not get points for reimplementing Lodash. Build a helper when the contract is narrow and tested. Use a library when the edge-case surface is broad. Use platform features like structuredClone and Object.groupBy when they match the contract.

Library and built-in equivalents
ChoiceExamplesWhen to use
Lodash_.cloneDeep, _.isEqual, _.curry, _.memoize, _.once, _.debounce, _.throttle, _.getUse it when browser support, bundle policy, and edge cases justify a proven dependency.
RamdaR.curry, R.compose, R.pipe, R.memoizeWith, R.pathUse it when a codebase already embraces data-last functional style.
Built-insstructuredClone, Object.groupBy, Map, Set, WeakMap, Object.entriesUse them first when they match the contract exactly.
Custom utilityA small function plus conformance testsUse it when you need one policy, not a general-purpose library surface.
Choose the utility strategy
  • Clone plain data, Dates, Maps, Sets, and cycles with no functions or class identity.
  • Compare many nested values across a product with years of edge cases.
  • Memoize one expensive selector with a known string key and manual clearing.
  • Flatten user keys that may already contain dots without an escaping policy.
  • Make an initialization callback run only once and preserve the first result.
  • A team already uses data-last helpers and wants point-free pipelines everywhere.
  • Group array items by a property in modern Node and browsers.
  • Deep clone live closures and DOM nodes for app state.
Try it yourself
0 of 8 correct

Sort each card by who should own the edge cases: the platform, a mature library, your code, or nobody because the shape is too ambiguous.

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

Common misconceptions

  • “Deep clone means copy everything.” No helper can safely clone closures, DOM identity, private fields, and every descriptor unless the contract says so.
  • “JSON round-trip is a clone.” It is a JSON data transform. Dates, Maps, Sets, functions, undefined, symbols, and cycles prove the difference.
  • “Deep equal is obvious.” NaN, signed zero, prototypes, Map order, Set matching, and cycles are policy choices.
  • “Currying should guess any function shape.” fn.length ignores rest parameters and stops at the first default parameter. Let callers pass arity when needed.
  • “Path strings are always safe.” Keys with dots or brackets need escaping or segment arrays.
Shortcut versus failing edge case
ShortcutFails whenProduction habit
Spread for cloneNested references must be independentUse spread only for shallow updates.
JSON.stringify(a) === JSON.stringify(b)Key order, NaN, Dates, Maps, or cycles appearUse an equality contract and tests.
JSON.stringify(args) as a memo keyObjects have different key order or identity mattersUse a resolver that matches the call site.
path.split('.') for all dataA key contains a dotEscape segments or accept segment arrays.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upWrite once

Fill in the helper, then type the three logs the completed program should print.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function once(fn) {
  // TODO: remember whether fn already ran and return the first result forever
}
let calls = 0;
const init = once(() => ++calls);
console.log(init());
console.log(init());
console.log(calls);

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

    Exercise 2 · Warm-upFix a deepEqual that fails on NaN

    Replace the primitive equality check so the program prints the desired value.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function equal(a, b) {
      if (a === b) return true;
      return false;
    }
    console.log(equal(NaN, NaN));

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

      Exercise 3 · PracticeWrite unflatten

      Implement the inverse for simple dotted paths and type the JSON string it should print.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function unflatten(flat) {
        const root = {};
        // TODO: split each path and create containers
        return root;
      }
      console.log(JSON.stringify(unflatten({ "user.name": "Ada", "list.0": "js" })));

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

        Exercise 4 · PracticeImplement get with defaults

        Support the path users[0].profile.name and return a default for missing users.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function get(obj, path, defaultValue) {
          // TODO: support dots, brackets, and a default for missing values
        }
        const data = { users: [{ profile: { name: "Ada" } }] };
        console.log(get(data, "users[0].profile.name", "Unknown"));
        console.log(get(data, "users[1].profile.name", "Unknown"));

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

          Exercise 5 · ChallengeChallenge: choose the prototype policy

          Assume the lesson's deepClone is in scope. Type the two values printed.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Item { constructor(id) { this.id = id; } }
          const copy = deepClone(new Item(7), { preservePrototype: false });
          console.log(copy instanceof Item);
          console.log(copy.id);

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

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why does the fixed deepClone write to a WeakMap before copying child properties?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this Object.is policy print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(Object.is(NaN, NaN));
              console.log(Object.is(+0, -0));

              Choose an answer to see the explanation.

            3. Question 3 of 7When does this curried call execute the original function?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const __ = Symbol("placeholder");
              const add3 = (a, b, c) => a + b + c;
              const curried = curry(add3);
              console.log(curried(__, 2)(1)(3));

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the memoized area example print after a cache hit?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let calls = 0;
              const area = memoize((w, h) => { calls++; return w * h; }, (w, h) => w + "x" + h);
              console.log(area(3, 4));
              console.log(area(3, 4));
              console.log(calls);

              Choose an answer to see the explanation.

            5. Question 5 of 7What does once preserve in this example?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const tool = { label: "save", init: once(function (suffix) { return this.label + suffix; }) };
              console.log(tool.init("!"));
              console.log(tool.init("?"));

              Choose an answer to see the explanation.

            6. Question 6 of 7What path appears for the first array item after flattening?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const flat = flattenObject({ list: ["js"], empty: [] });
              console.log(flat["list.0"]);
              console.log(Array.isArray(flat.empty));

              Choose an answer to see the explanation.

            7. Question 7 of 7Why is a path string API risky for arbitrary user keys?

              Choose an answer to see the explanation.

            Key takeaways

            • Utility names are not enough; write the supported inputs, unsupported inputs, and edge-case policies.
            • deepClone and deepEqual become reliable only when cycles, special objects, prototypes, and equality policy are tested.
            • curry, compose, and pipe should keep call sites clearer, not cleverer.
            • memoize needs a key resolver and visible cache controls; once must preserve the first receiver, arguments, and result.
            • Flattened paths require array, empty-container, escaping, inverse, and default-value policies.

            Remember the one-liner.
            Build utility functions like small libraries: contract first, naive failure second, hardened implementation third, conformance tests always.

            Next in this module, build an event emitter turns callbacks into a tiny subscription system. The same discipline applies: state the behavior, build the minimal API, and prove the edge cases.

            CompleteFrontend Clear concepts. Working examples.