Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I2

Objects deeply

The rest of the object/array toolbox — past what B5 already covered.

The first pass at objects and arrays covered literals, the core mutating/non-mutating array methods, and basic destructuring/spread. This chapter is everything past that: computed behavior on objects, the two collection types that aren't arrays, and the JSON details that only bite in real apps.

Getters and setters

A property that runs code on read or write, while still looking like a plain field to anything using it — no () at the call site.

const person = {
  first: "Ana",
  last: "Rao",
  get fullName() {
    return this.first + " " + this.last;
  },
  set fullName(value) {
    [this.first, this.last] = value.split(" ");
  },
};

console.log(person.fullName);      // what happens?
person.fullName = "Ravi Shah";     // looks like a plain assignment
console.log(person.first, person.last);   // what happens?

"Ana Rao", then "Ravi" "Shah" — the setter ran and split the incoming string back into two real fields. This is the standard way to keep a derived value (fullName) in sync with the data it's derived from, without callers ever calling a method to get it.

Object statics — the whole-object toolkit

Call Returns
Object.keys(obj)array of own, enumerable key names
Object.values(obj)array of the matching values
Object.entries(obj)array of [key, value] pairs — feeds straight into a for...of or new Map()
Object.fromEntries(pairs)the reverse — pairs back into an object
Object.assign(target, ...sources)copies own enumerable props from each source onto target, left to right — mutates target
Object.hasOwn(obj, key)true only for the object's own property, never an inherited one
const o = { a: 1, b: 2 };
Object.entries(o);                       // [["a", 1], ["b", 2]]
Object.fromEntries([["x", 1], ["y", 2]]); // { x: 1, y: 2 }
Object.hasOwn(o, "a");                   // true
Object.hasOwn(o, "toString");            // false — toString is inherited, not o's own
⚠ Object.assign mutates its first argument Object.assign(base, patch) changes base in place and returns it. To merge without touching either input, pass an empty object as the target — or reach for spread instead: { ...base, ...patch } does the same merge, immutably.

Copying — shallow, deep, and what actually does which

Already established: spread and Object.assign both copy one level deep, so a nested object stays shared. For a real deep copy of plain data, structuredClone() is the built-in answer — no library, works on objects, arrays, Date, Map, Set, and it correctly throws rather than silently mangling a function or a DOM node it can't clone.

const original = { nested: { count: 1 }, tags: new Set(["a"]) };
const deep = structuredClone(original);
deep.nested.count = 99;
console.log(original.nested.count);   // 1 — untouched, unlike a spread copy
console.log(deep.tags instanceof Set); // true — the Set survived the clone

Map and Set — objects and arrays with better rules

A Map is a key/value store like an object, but with two things a plain object can't do: any value can be a key (not just strings/symbols), and it remembers insertion order reliably, including for keys that look numeric.

const objKey = { id: 1 };
const cache = new Map();
cache.set(objKey, "cached result");
cache.set("plain-string-key", "also fine");

console.log(cache.get(objKey));         // what happens?
console.log(cache.get({ id: 1 }));      // a DIFFERENT object, same shape — what happens?
console.log(cache.size);

"cached result", then undefined. Map keys are compared by identity, same as everything else about object references — a freshly-built { id: 1 } is not the objKey it was stored under, no matter how identical it looks. This is exactly why an object can be used to key a private, un-guessable cache entry.

Object Map
Key typesstrings and symbols onlyanything — objects, functions, NaN
SizeObject.keys(o).lengthmap.size, directly
Iteration ordermostly insertion, but integer-like keys sort first — a real gotchaalways insertion order, no exceptions
Extra baggageinherits from Object.prototype (toString, etc.)starts empty — nothing to accidentally collide with

Set is the same idea for values with no key at all — a list that silently refuses duplicates, compared the same way Map keys are:

const unique = new Set([1, 2, 2, 3, 3, 3]);
[...unique];              // [1, 2, 3]
unique.has(2);             // true
[...new Set(array)];       // the standard one-liner for "de-duplicate this array"

WeakMap and WeakSet

Same idea as Map/Set, with one restriction and one superpower: keys (or values, for a WeakSet) must be objects, and they're held weakly — if nothing else in the program references that object anymore, the garbage collector is free to remove it, entry and all.

const wm = new WeakMap();
let el = { id: "temp" };
wm.set(el, { extra: "metadata tied to el's lifetime" });
el = null;   // no other reference to the object exists anymore —
             // the WeakMap's entry can now be garbage collected too
Rule Reach for a WeakMap when you're attaching extra data to objects you don't own the lifetime of — DOM nodes, other modules' objects — so that data doesn't accidentally keep them alive forever. A regular Map would hold a strong reference and leak memory as long as the map itself exists.

Array methods B5 didn't cover

Call Does
Array.from(iterable, mapFn?)builds a real array from anything iterable OR array-like — a string, a Set, a { length: n } object — with an optional map step built in
arr.flat(depth)flattens nested arrays depth levels (default 1)
arr.flatMap(fn).map(fn).flat(1), done in one pass — for when a mapper sometimes returns 0 or several items per input
arr.at(-1)same as arr[arr.length - 1], but works with negative indices directly
Array.from({ length: 3 }, (_, i) => i * 2);   // [0, 2, 4] — no real array needed to start
Array.from("abc");                             // ["a", "b", "c"]
[1, [2, [3, [4]]]].flat(2);                    // [1, 2, 3, [4]] — only 2 levels deep
[1, 2, 3].flatMap(x => [x, x * 10]);           // [1, 10, 2, 20, 3, 30]
[1, 2, 3].at(-1);                              // 3

Sort stability

Modern Array.prototype.sort is guaranteed stable: elements that compare equal keep their original relative order. That's not a minor implementation detail — it's what makes multi-key sorting possible with two simple, separate sorts.

const items = [
  { key: "a", group: 1 },
  { key: "b", group: 1 },
  { key: "c", group: 0 },
];
const sorted = items.sort((x, y) => x.group - y.group);
console.log(sorted.map((i) => i.key));   // what happens?

["c", "a", "b"] — "a" and "b" both have group: 1, tied under the comparator, and stability guarantees they stay in their original relative order (a before b) rather than the sort being free to swap them arbitrarily.

JSON — replacer and reviver

Both stringify and parse take an optional second function that runs on every key/value pair — a hook to filter or transform as the conversion happens, instead of after.

JSON.stringify(
  { name: "Ana", email: "ana@x.com", passwordHash: "…" },
  (key, value) => (key === "passwordHash" ? undefined : value)
);   // {"name":"Ana","email":"ana@x.com"} — dropped before it ever became text

JSON.parse(
  '{"createdAt":"2024-01-01T00:00:00.000Z"}',
  (key, value) => (key === "createdAt" ? new Date(value) : value)
);   // { createdAt:  } — JSON has no date type, so this is how you get one back

The replacer can also be an array instead of a function — a plain allow-list of key names to keep, everything else dropped. Simpler when you just need a fixed subset of fields, no per-key logic.

Nested destructuring and defaults, past the basics

function render({
  user: { name, address: { city = "Unknown" } = {} } = {},
  theme = "light",
} = {}) {
  return name + " · " + city + " · " + theme;
}
render({ user: { name: "Ana" } });   // "Ana · Unknown · light"
render();                             // no crash — every level has a fallback

Each = {} is a default for that specific level — without it, destructuring a level that's missing (like address not existing on a bare { name: "Ana" }) throws instead of quietly falling through, because you can't destructure a property off of undefined.

Optional chaining meets deep data

const config = { server: { retries: 0 } };

config.server?.timeout ?? 5000;    // 5000 — timeout doesn't exist, ?? catches it
config.server?.retries ?? 5000;    // 0 — retries DOES exist, so its real value wins
config.client?.host ?? "localhost";  // "localhost" — client itself is missing, chain stops safely

That middle line is the one worth sitting with: ?? only falls back on null/undefined, so a genuinely present 0 survives untouched — exactly the combination (?. to reach safely, ?? to default correctly) that a plain config.server && config.server.retries || 5000 gets wrong, because || would treat that real 0 as missing too.

Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

groupBy with reduce5 tests · intermediatesort() without wrecking the original4 tests · intermediateSet to dedupe, Map to count4 tests · intermediate
←previousScope & functions, properly↑ CovernextPrototypes & OOP→