067: 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.
- Why prefer
const, and when isletrequired? - What are the failure results of
find()andfindIndex()? - Which mutate:
map,filter,sort,toSorted, object spread? - Why can a new array still contain shared object references?
- What initial value should a subtotal reduction usually use?
- Why is
[].every(predicate)true, and what cart check must accompany it? - In
{ ...defaults, ...user, role: "customer" }, which role wins? - What does a browser entry need to use static imports?
- When should code throw instead of returning validation issues?
- 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:
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:
| Need | Best starting tool | Result |
|---|---|---|
| one output per input | map() | new same-length array |
| all matching values | filter() | new array |
| first matching value | find() | element or undefined |
| first matching position | findIndex() | index or -1 |
| at least one passes | some() | boolean |
| all pass | every() | boolean |
| one total | reduce() | accumulated value |
| copied sorted order | toSorted() | new shallow array |
| intentional in-place order | sort() | 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:
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:
["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:
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:
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:
{ 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:
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:
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) fromnull(intentional empty marker). - Use
??when0,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, notmap, 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
constdoes 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
Errorobjects; 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
constby default andletfor 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.
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:
["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.
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.
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:
{ 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:
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
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
shallowcontained 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
- MDN: JavaScript Guide
- MDN:
Array - MDN: Working with objects
- MDN: Spread syntax
- MDN: JavaScript modules
- MDN: Error handling
- ECMA-262 current specification
- sudheerj/javascript-interview-questions
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.
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
- Walk a missing property through every prototype until
null. - Explain
Constructor.prototypeversusObject.getPrototypeOf(instance)and why__proto__should not be used in new code. - Compare
in,Object.hasOwn(), andpropertyIsEnumerable(). - Give a concrete prototype-pollution attack and two defenses.
- Choose path copying,
structuredClone, JSON, or a custom clone for four data shapes, including one cyclic and one class-instance shape. - Implement and test debounce with
cancel/flush, and throttle with explicit leading/trailing policy. - What cache key and invalidation policy does memoization require?
- Explain
once, currying, right-to-left composition, and recursive flattening. - Design an emitter's unsubscribe,
once, listener-error, and mutation rules. - Implement
Promise.allandPromise.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.
