FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·067·10 MIN READ

067: Core JavaScript Review and Assessment

TOPICS COVERED: Core JavaScript Review and Assessment

Learning outcomes

By the end of this lesson, you should be able to:

  • explain core JavaScript choices from values through modules and errors;
  • solve unfamiliar product and cart problems without reaching for new syntax;
  • choose array methods by intent and predict whether an operation mutates data;
  • update nested data in an immutable style;
  • identify your own weak areas and apply targeted debugging steps.

Retrieval assessment

Answer these before opening your notes or running code. The point is to find what you can currently retrieve, not to guess perfectly.

  1. Why prefer const, and when is let required?
  2. What are the failure results of find() and findIndex()?
  3. Which of these mutate: map, filter, sort, toSorted, and 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 in order 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?

Score this checkpoint one point per answer. A score of 8-10 means you can proceed. With 5-7, review the matching section as you solve the exercises. Below 5, trace the worked example in writing before continuing. Do not introduce new syntax today.

Vocabulary check

These terms are worth keeping precise because they recur throughout the review:

  • Primitive: “Data that is not an object and has no methods.” — Source: MDN: Primitive
  • Callback: A function passed to another operation so that operation can invoke it later. — Source: MDN: Callback function
  • Predicate: A callback interpreted as truthy or falsy (course term).
  • Accumulator: The carried result threaded through reduction passes. — Source: MDN: reduce
  • Mutation: A change made to an existing value or container (course term).
  • Shallow copy: A new outer container that retains shared references to nested values. — Source: MDN: Spread syntax
  • Structural sharing: The intentional reuse of branches that did not change (course term).
  • Module: A scoped file unit connected to other files through imports and exports. — Source: MDN: JavaScript modules
  • Exception: A thrown value that interrupts normal flow until it is handled. — Source: MDN: try...catch
  • Invariant: A rule that 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

Most JavaScript application logic can be understood as 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 give values names. Functions describe input-to-output behavior. Arrays hold ordered collections, objects describe the fields of a record, and array callbacks express repeated operations. Spread creates a new outer container; a nested update must copy each changed path. Modules separate responsibilities, while errors report operations that cannot fulfill their contracts.

When a method-choice question appears, start with intent:

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

This table is a starting point, not a reason to chain every operation together. A named intermediate value or a for...of loop is often the clearer choice.

Worked review example: explain before running

Before running this code, predict every output and every reference comparison. The reference checks are as important as the values.

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

Filtering removes the sold-out Backpack from the labels, while sorting creates a copied outer array. In this data, the original order already happens to be ascending by price. The update maps to a new array, reuses the unchanged Notebook, copies the Water Bottle, and copies its changed supplier as well. Stock is 12 + 5 + 0 = 17. A deep clone was not needed because only one nested path changed.

Intermediate review: unseen checkout problem

Problem: A cart contains product IDs and quantities. Produce a summary containing valid detailed lines, issue messages for invalid lines, a subtotal, and canCheckout. A cart cannot be checked out when it is empty or when any issue exists. Do not mutate the inputs.

Start by stating the data flow in plain language:

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

Because the operation builds two collections at once, a local loop is easier to read than a reduction that repeatedly copies two accumulator arrays:

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
}

Choosing a loop here is not a failure to use array methods. It makes the two output collections explicit. The numeric aggregation is still a natural use of reduce().

Optional advanced extension: module sketch

Without creating extra files, assign each responsibility like this:

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

For example:

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

cart.js should receive products as an input rather than importing mutable catalog state. Explicit inputs make the function easier to reuse and test, and keep the dependency direction visible.

Mistakes and targeted debugging

Values and conditions

  • Prefer === and !==; coercive equality can hide type mistakes.
  • Distinguish undefined, which often means absent, from null, which is commonly an intentional empty marker.
  • Use ?? when 0, false, and "" are valid values that should not trigger a fallback.

Functions

  • Return values from reusable logic instead of logging inside it.
  • A callback body with braces needs an explicit return.
  • Keep each function focused on one business operation.

Arrays

  • Use filter, not map, when the goal is removal.
  • Compare findIndex() with -1; never rely on the truthiness of its result.
  • Provide an initial value to reduce().
  • Use numeric comparators and remember that sort() mutates.

Objects and copies

  • const prevents rebinding; it does not freeze an object.
  • Spread is shallow, so compare references at each relevant path.
  • For a nested update, copy the root, every containing array or object, and the changed record.

Modules and errors

  • Use native ESM with the correct named/default syntax, explicit browser paths, type="module", and HTTP serving.
  • Throw Error objects, and catch only where the code can recover or present the failure usefully.
  • Before changing code, read the error name, message, and first relevant stack frame owned by your code.

A disciplined debugging loop is: reproduce with the smallest data, predict the result, log intermediate shapes and references, find the first wrong value, fix the code that produced it, and rerun both normal and boundary cases.

Best practices checklist

  • Use const by default and let only for intentional reassignment.
  • Model consistent records with stable IDs.
  • Validate and normalize at boundaries before changing state.
  • Choose methods by intent and 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; do not swallow unexpected errors.

Exercises: mini assessment

Core

From products, return the 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, while reduce() returns a number. The temporary filtered array still shares the product objects, but none of these callbacks changes those objects.

Practice

Write restockSupplier(products, supplierName, amount). It must validate that amount is a positive integer and return 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 did not change, so it can be structurally shared. Only the product's stock field changed.

Professional Extension

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

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 an unseen problem): values/conditions, functions, arrays, objects/copies, modules, and errors. Revisit the lowest-scoring area with one small product example. Predict it, run it, and explain both the output and the mutation behavior. The goal is not to memorize punctuation; it is to trace data accurately and choose 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, identify the binding, mutation, call site, and evaluation order before guessing. These short traces expose several high-value JavaScript behaviors:

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 continue with the follow-up instead of stopping at the 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 compares identity, and which observes a value?

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 and then walk the prototype chain. For Map questions, distinguish a missing key from a key whose stored value is undefined.

The public question bank at sudheerj/javascript-interview-questions is useful for additional prompts. Treat it as a prompt source rather than 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 the code.

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 writing code: may inputs be cyclic, must functions or classes survive copying, do callbacks run on a leading or trailing edge, is event delivery synchronous, and is promise work 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 an 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.
Reader page: /javascript/lesson/067/core-javascript-review-and-assessment