Module: JavaScript
JavaScript·067·8 MIN READ

067: Core JavaScript Review and Assessment

TOPICS COVERED: Core JavaScript Review and Assessment

Learning outcomes

By the end of this lesson, you can:

  • explain core JavaScript choices from values through modules and errors;
  • solve unseen product/cart problems without adding new syntax;
  • choose array methods by intent and predict mutation behavior;
  • update nested data in an immutable style;
  • identify personal weak areas and use targeted debugging steps.

Retrieval assessment

Answer before opening notes or running code.

  1. Why prefer const, and when is let required?
  2. What are the failure results of find() and findIndex()?
  3. Which mutate: map, filter, sort, toSorted, object spread?
  4. Why can a new array still contain shared object references?
  5. What initial value should a subtotal reduction usually use?
  6. Why is [].every(predicate) true, and what cart check must accompany it?
  7. In { ...defaults, ...user, role: "customer" }, which role wins?
  8. What does a browser entry need to use static imports?
  9. When should code throw instead of returning validation issues?
  10. Which containers must be copied to update an item inside one order inside a store?

Checkpoint scoring: one point each. 8-10 means proceed; 5-7 means review the matching section while solving; below 5 means trace the worked example in writing before continuing. Do not introduce new syntax today.

Vocabulary check

  • Primitive: “Data that is not an object and has no methods.” — Source: MDN: Primitive
  • Callback: Function passed to another operation for later invocation. — Source: MDN: Callback function
  • Predicate: callback interpreted as truthy/falsy (course term).
  • Accumulator: Carried result threaded through reduction passes. — Source: MDN: reduce
  • Mutation: change to an existing value/container (course term).
  • Shallow copy: New outer container retaining nested shared references. — Source: MDN: Spread syntax
  • Structural sharing: intentional reuse of unchanged branches (course term).
  • Module: Scoped file unit connected by imports/exports. — Source: MDN: JavaScript modules
  • Exception: Thrown value interrupting normal flow until handled. — Source: MDN: try...catch
  • Invariant: rule valid application state must preserve (course term).
  • Interview pattern (official): "An interview pattern is a reusable strategy for recognizing and solving unseen problems (e.g., map/filter/reduce, two-pointer)." — Source: MDN: Debugging JavaScript
  • Unseen problem (course): "A problem you have not solved before, requiring you to apply principles rather than recall syntax." — Source: course synthesis — interview practice; see MDN: Debugging JavaScript

Beginner mental model: one connected system

JavaScript application logic is a flow of values:

text
input
  -> validate types and business rules
  -> transform arrays/objects with focused functions
  -> return next state or derived summary
  -> entry module displays output or handles an error

Variables label values. Functions define input-to-output behavior. Arrays hold ordered collections. Objects label the fields of one record. Array callbacks express repeated operations. Spread creates new outer containers; nested updates copy changed paths. Modules organize responsibilities. Errors report operations that cannot fulfill their contracts.

Method-choice questions:

NeedBest starting toolResult
one output per inputmap()new same-length array
all matching valuesfilter()new array
first matching valuefind()element or undefined
first matching positionfindIndex()index or -1
at least one passessome()boolean
all passevery()boolean
one totalreduce()accumulated value
copied sorted ordertoSorted()new shallow array
intentional in-place ordersort()same mutated array

The table is a starting point, not a challenge to chain everything. A named intermediate value or for...of loop can be clearer.

Worked review example: explain before running

Predict every output and reference comparison:

js
const products = [
  {
    id: "p1",
    name: "Notebook",
    price: 4,
    stock: 12,
    supplier: { name: "Paper Co" },
  },
  {
    id: "p3",
    name: "Water Bottle",
    price: 16,
    stock: 7,
    supplier: { name: "Hydrate Ltd" },
  },
  {
    id: "p4",
    name: "Backpack",
    price: 45,
    stock: 0,
    supplier: { name: "Carry Co" },
  },
];

const availableLabels = products
  .filter((product) => product.stock > 0)
  .map(({ name, price }) => `${name}: $${price}`);

const affordableFirst = products.toSorted((a, b) => a.price - b.price);

const updatedProducts = products.map((product) =>
  product.id === "p3"
    ? {
        ...product,
        stock: 5,
        supplier: { ...product.supplier, name: "Hydrate Partners" },
      }
    : product,
);

const stockTotal = updatedProducts.reduce(
  (total, product) => total + product.stock,
  0,
);

console.log(availableLabels);
console.log(affordableFirst.map((product) => product.id));
console.log(products[1].stock);
console.log(updatedProducts[1].stock);
console.log(updatedProducts[0] === products[0]);
console.log(updatedProducts[1] === products[1]);
console.log(updatedProducts[1].supplier === products[1].supplier);
console.log(stockTotal);

Output:

text
["Notebook: $4", "Water Bottle: $16"]
["p1", "p3", "p4"]
7
5
true
false
false
17

Explanation: filtering removes sold-out Backpack from labels, while sorting produces a copied outer array. The source happened to already be price-ascending. Update maps to a new array, reuses unchanged Notebook, copies Water Bottle, and also copies its changed supplier. Stock is 12 + 5 + 0 = 17. Nothing required a deep clone.

Intermediate review: unseen checkout problem

Problem: A cart contains product IDs and quantities. Produce a summary with valid detailed lines, issue messages for invalid lines, subtotal, and canCheckout. A cart cannot checkout if empty or if any issue exists. Do not mutate inputs.

First state data flow verbally:

text
cart lines -> join product -> classify valid/invalid
valid lines -> line totals -> subtotal
cart length + issues -> canCheckout

One readable solution uses a local loop because it produces two collections at once:

js
function reviewCart(cart, products) {
  const lines = [];
  const issues = [];

  for (const cartLine of cart) {
    const product = products.find(
      (item) => item.id === cartLine.productId,
    );

    if (!product) {
      issues.push(`Missing product: ${cartLine.productId}`);
      continue;
    }

    if (
      !Number.isInteger(cartLine.quantity) ||
      cartLine.quantity < 1 ||
      cartLine.quantity > product.stock
    ) {
      issues.push(`Invalid quantity for ${product.name}`);
      continue;
    }

    lines.push({
      productId: product.id,
      name: product.name,
      quantity: cartLine.quantity,
      lineTotal: product.price * cartLine.quantity,
    });
  }

  const subtotal = lines.reduce(
    (total, line) => total + line.lineTotal,
    0,
  );

  return {
    lines,
    issues,
    subtotal,
    canCheckout: cart.length > 0 && issues.length === 0,
  };
}

const cart = [
  { productId: "p1", quantity: 2 },
  { productId: "p3", quantity: 8 },
  { productId: "missing", quantity: 1 },
];

console.log(reviewCart(cart, products));

Output:

text
{
  lines: [
    { productId: "p1", name: "Notebook", quantity: 2, lineTotal: 8 }
  ],
  issues: [
    "Invalid quantity for Water Bottle",
    "Missing product: missing"
  ],
  subtotal: 8,
  canCheckout: false
}

This is not a failure to use array methods. A loop is clearer than repeatedly copying two accumulator arrays inside reduce(). The numeric aggregation remains a natural reduction.

Optional advanced extension: module sketch

Without writing extra files, assign responsibilities:

text
catalog.js     named exports: findProduct, updateProduct
cart.js        named exports: addToCart, reviewCart, removeFromCart
validation.js named exports: validateProduct, validateQuantity
format.js      default export: formatCurrency
main.js        owns current state, imports functions, handles output/errors

Example import line:

js
import { reviewCart, addToCart } from "./cart.js";
import formatCurrency from "./format.js";

Explain why cart.js should receive products rather than import mutable catalog state: explicit inputs improve reuse, testing, and dependency direction.

Mistakes and targeted debugging

Values and conditions

  • Prefer ===/!==; coercive equality hides type mistakes.
  • Distinguish undefined (often absent) from null (intentional empty marker).
  • Use ?? when 0, false, and "" are valid.

Functions

  • Return values instead of logging inside reusable logic.
  • A callback with braces needs return.
  • Keep one function focused on one business operation.

Arrays

  • Use filter, not map, to remove.
  • Check findIndex() against -1; never use its truthiness.
  • Supply a reduce initial value.
  • Use numeric comparators and remember sort() mutates.

Objects and copies

  • const does not freeze objects.
  • Spread is shallow; compare references at each path.
  • For nested updates, copy root, each containing array/object, and changed record.

Modules and errors

  • Use native ESM, correct named/default syntax, explicit browser paths, type="module", and HTTP serving.
  • Throw Error objects; catch only where recovery or presentation is possible.
  • Read name, message, and first relevant owned stack frame before changing code.

A disciplined debugging loop is: reproduce with smallest data, predict, log intermediate shapes/references, identify the first wrong value, fix its producer, then rerun normal and boundary cases.

Best practices checklist

  • Use const by default and let for intentional reassignment.
  • Model consistent records with stable IDs.
  • Validate and normalize at boundaries before state changes.
  • Choose methods by intent; prioritize readable data flow.
  • Preserve inputs with immutable-style writes and deliberate structural sharing.
  • Keep calculations numeric until display formatting.
  • Organize standard ESM modules by responsibility.
  • Make failures actionable and do not swallow unexpected errors.

Exercises: mini assessment

Core

From products, return available product names and calculate total stock. Then explain whether either operation mutates products.

js
const names = products
  .filter((product) => product.stock > 0)
  .map((product) => product.name);
const totalStock = products.reduce(
  (total, product) => total + product.stock,
  0,
);

console.log(names);
console.log(totalStock);

Output:

text
["Notebook", "Water Bottle"]
19

Neither operation mutates the products array. filter() and map() create arrays; reduce() returns a number. Product objects remain shared in the temporary filtered array, but these callbacks do not change them.

Practice

Write restockSupplier(products, supplierName, amount) that validates a positive integer amount and returns updated products without changing nested supplier objects.

js
function restockSupplier(products, supplierName, amount) {
  if (!Number.isInteger(amount) || amount < 1) {
    throw new RangeError("Restock amount must be a positive integer");
  }

  return products.map((product) =>
    product.supplier.name === supplierName
      ? { ...product, stock: product.stock + amount }
      : product,
  );
}

const restocked = restockSupplier(products, "Hydrate Ltd", 5);
console.log(restocked[1].stock); // 12
console.log(products[1].stock);  // 7
console.log(restocked[1].supplier === products[1].supplier); // true

The supplier object is unchanged and can be structurally shared. Only the product's stock changed.

Professional Extension

Write checkout(cart, products, percentDiscount = 0). Validate discount 0..100; use reviewCart; throw if checkout is impossible; return subtotal, discount, and total. Preserve the original error when adding checkout context.

js
function checkout(cart, products, percentDiscount = 0) {
  try {
    if (
      typeof percentDiscount !== "number" ||
      !Number.isFinite(percentDiscount) ||
      percentDiscount < 0 ||
      percentDiscount > 100
    ) {
      throw new RangeError("Discount must be between 0 and 100");
    }

    const review = reviewCart(cart, products);
    if (!review.canCheckout) {
      const message = review.issues.length > 0
        ? review.issues.join("; ")
        : "Cart is empty";
      throw new Error(message);
    }

    const discount = review.subtotal * percentDiscount / 100;
    return {
      subtotal: review.subtotal,
      discount,
      total: review.subtotal - discount,
    };
  } catch (error) {
    throw new Error("Checkout failed", { cause: error });
  }
}

try {
  const validCart = [
    { productId: "p1", quantity: 2 },
    { productId: "p3", quantity: 1 },
  ];
  console.log(checkout(validCart, products, 10));
} catch (error) {
  console.error(error.message, error.cause?.message);
}

Output:

text
{ subtotal: 24, discount: 2.4, total: 21.6 }

Recap and remediation

Score each area 0 (cannot explain), 1 (can follow), or 2 (can solve unseen): values/conditions, functions, arrays, objects/copies, modules, errors. Revisit the lowest area with one tiny product example, predict it, run it, and explain output and mutation. The goal is not memorizing punctuation; it is accurately tracing data and choosing a clear operation.

Exit questions: Why should reduce() not replace every array method? What exact boundary makes spread shallow? How do module boundaries improve product logic? What is your first debugging action after a runtime error?

Interview output traces and follow-ups

For an output question, mark the binding, mutation, call site, and evaluation order before guessing. These are short but high-value traces:

js
const original = { value: 1 };
const shallow = { ...original };
const alias = original;
shallow.value += 1;
alias.value += 1;

console.log(original.value, shallow.value); // 2 2
console.log(original === alias, original === shallow); // true false
js
const makeValue = () => {
  let value = 0;
  return () => value++;
};
const read = makeValue();
console.log(read(), read(), read()); // 0 1 2

Explain each answer, then ask the follow-up rather than stopping at output:

  • How would you deep-copy the first graph, and what values could make that fail?
  • Which line would change if shallow contained a nested object?
  • How would you make the counter resettable or expose its state safely?
  • Which statement is an identity comparison and which is a value observation?

Use the same method for this: identify the expression before the final dot, then account for detachment, call/apply/bind, or arrow lexical capture. For prototype questions, identify own properties first, then walk the prototype chain. For Map questions, distinguish a missing key from a stored undefined.

The public question bank at sudheerj/javascript-interview-questions is useful for additional prompts. Treat it as a prompt source, not an authority: write your own explanation, run the trace in a modern runtime, and confirm language semantics against MDN or ECMA-262.

Official references

High-value JavaScript gap lab

Use this final lab to connect object mechanics, copying, functional utilities, events, and promises. Predict the assertions before running them.

js
const base = { role: "user" };
const account = Object.create(base);
account.name = "Maya";
console.assert("role" in account);
console.assert(!Object.hasOwn(account, "role"));
console.assert(Object.getPrototypeOf(account) === base);

const source = { nested: { count: 1 } };
const shallow = { ...source };
shallow.nested.count = 2;
console.assert(source.nested.count === 2); // shallow boundary

const independent = structuredClone(source);
independent.nested.count = 3;
console.assert(source.nested.count === 2);

const values = [Promise.resolve("first"), "second"];
Promise.all(values).then((result) => {
  console.assert(JSON.stringify(result) === JSON.stringify(["first", "second"]));
});

For an interview answer, state the contract before code: whether inputs may be cyclic, whether functions/classes must survive copying, whether callbacks run leading or trailing, whether event delivery is synchronous, and whether promise work is cancellable. A correct implementation with an unstated contract is still ambiguous production code.

Interview question bank

  1. Walk a missing property through every prototype until null.
  2. Explain Constructor.prototype versus Object.getPrototypeOf(instance) and why __proto__ should not be used in new code.
  3. Compare in, Object.hasOwn(), and propertyIsEnumerable().
  4. Give a concrete prototype-pollution attack and two defenses.
  5. Choose path copying, structuredClone, JSON, or a custom clone for four data shapes, including one cyclic and one class-instance shape.
  6. Implement and test debounce with cancel/flush, and throttle with explicit leading/trailing policy.
  7. What cache key and invalidation policy does memoization require?
  8. Explain once, currying, right-to-left composition, and recursive flattening.
  9. Design an emitter's unsubscribe, once, listener-error, and mutation rules.
  10. Implement Promise.all and Promise.race; explain order, fail-fast behavior, thenables, empty input, and why neither cancels underlying operations.

Verification checklist

  • Assert output values and identity comparisons, not output alone.
  • Test empty arrays, missing keys, inherited keys, falsy values, cycles, and unsupported clone values.
  • Test repeated, cancelled, flushed, leading, and trailing timed calls.
  • Test emitter unsubscribe, once, self-removal, duplicate listeners, and listener errors.
  • Test promise plain values, completion order versus result order, rejection, empty input, and thenables.