Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I2
19 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI2 · Part 2 · Intermediate

Objects deeply

The rest of the object/array toolbox — past what the beginner chapter covered.

8 min read15 sections4 exercises
19 of 41

In this chapter

  • Getters and setters
  • Object statics — the whole-object toolkit
  • Copying — shallow, deep, and what actually does which
  • Map and Set — objects and arrays with better rules
  • WeakMap and WeakSet

and 10 more

about 8 min
15On this page
  1. Getters and setters
  2. Object statics — the whole-object toolkit
  3. Copying — shallow, deep, and what actually does which
  4. Map and Set — objects and arrays with better rules
  5. WeakMap and WeakSet
  6. Array methods the beginner chapter skipped
  7. The immutable array twins — toSorted, toReversed, toSpliced, with
  8. findLast and findLastIndex
  9. Object.groupBy — grouping without a reduce
  10. Set gets algebra: union, intersection, difference
  11. Locking an object down: seal, freeze, preventExtensions
  12. Sort stability
  13. JSON — replacer and reviver
  14. Nested destructuring and defaults, past the basics
  15. Optional chaining meets deep data

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.

Dry run: which lookups actually hit

CallKey compared againstSame reference as objKey?Result
cache.set(objKey, "cached result")——entry stored under objKey itself
cache.set("plain-string-key", "also fine")——a second, unrelated entry — size is now 2
cache.get(objKey)objKeyyes — literally the same object"cached result"
cache.get({ id: 1 })a brand-new object, same shapeno — different object in memoryundefined

Nothing about the shape of the key matters to a Map — only whether it's the exact same reference (or an identical primitive). Rebuilding an object with the same fields never gets you back the entry it was stored under.

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 the beginner chapter skipped

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

The immutable array twins — toSorted, toReversed, toSpliced, with

Four methods added specifically because their mutating originals are a common accidental-mutation bug: .sort(), .reverse(), .splice(), and index assignment each now have a non-mutating sibling that returns a brand-new array and leaves the original untouched.

const original = [3, 1, 2];

original.toSorted();       // [1, 2, 3] — original is untouched
original.toReversed();     // [2, 1, 3]
original.toSpliced(1, 1);  // [3, 2] — removes index 1, returns the result, doesn't touch original
original.with(0, 99);      // [99, 1, 2] — like original[0] = 99, but as a copy

console.log(original);     // [3, 1, 2] — every one of the four left it exactly as it was

Before these existed, the standard workaround was [...original].sort() — copy first, then mutate the copy. The to*/with methods do the same thing in one call, and read as intent rather than a defensive habit: "give me a sorted copy," not "copy this so it's safe to sort."

findLast and findLastIndex

const orders = [
  { id: 1, status: "shipped" },
  { id: 2, status: "pending" },
  { id: 3, status: "shipped" },
];

orders.find((o) => o.status === "shipped");       // order 1 — first match
orders.findLast((o) => o.status === "shipped");   // order 3 — last match, still one pass, just walked from the end

Before findLast, "the most recent matching item" meant reversing the array first (an allocation) or writing the loop backward by hand. It's the same relationship .find and .findLast now have that .indexOf and .lastIndexOf already had for plain values.

Object.groupBy — grouping without a reduce

const people = [
  { name: "Ana", team: "core" },
  { name: "Ravi", team: "core" },
  { name: "Mei", team: "infra" },
];

const byTeam = Object.groupBy(people, (p) => p.team);
console.log(byTeam);
// { core: [{name:"Ana",...}, {name:"Ravi",...}], infra: [{name:"Mei",...}] }

This used to be a reduce everyone wrote slightly differently — start an accumulator object, check whether the key exists yet, push or initialize. Object.groupBy is exactly that pattern, standardized and readable at the call site. Map.groupBy is the same idea with a real Map as the result, useful the moment a group key isn't a plain string.

Set gets algebra: union, intersection, difference

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

a.union(b);                 // Set(4) {1, 2, 3, 4}
a.intersection(b);          // Set(2) {2, 3}
a.difference(b);            // Set(1) {1} — in a, not in b
a.symmetricDifference(b);   // Set(2) {1, 4} — in exactly one of the two
a.isSubsetOf(new Set([1, 2, 3, 4]));   // true

Before these, a set operation meant dropping to [...a].filter((x) => b.has(x)) and back into a new Set(...) — correct, but re-derived by hand every time. These five are the textbook set-theory operations, built in, and every one returns a brand-new Set without touching either input.

Locking an object down: seal, freeze, preventExtensions

CallAdd new keys?Delete keys?Change existing values?
Object.preventExtensions(o)noyesyes
Object.seal(o)nonoyes
Object.freeze(o)nonono
const config = Object.freeze({ retries: 3 });
config.retries = 99;          // silently does nothing in sloppy mode, throws a TypeError in strict mode/modules
console.log(config.retries);  // 3 — unchanged either way
⚠ freeze is shallow, exactly like spread Object.freeze({ nested: { count: 1 } }) only locks the top level — frozen.nested.count = 99 works fine, because nested itself was never frozen. A genuinely deep freeze means walking every nested object and freezing each one individually.

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: <a real Date object> } — 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.

Interview answer, one sentence

"Copies are shallow unless you make them deep — spread and Object.assign copy one level, structuredClone copies deeply but not functions — Map and Set beat plain objects for arbitrary keys and uniqueness, and toSorted and its family give non-mutating versions of the array methods."

Before you move on
  • Explain why { ...base } and structuredClone(base) behave differently on an object with a nested field.
  • Say why cache.get({ id: 1 }) misses an identical-looking stored object, in terms of how Map compares keys.
  • Pick Map over a plain object for a specific reason (key type, iteration order, or no inherited baggage) instead of by habit.
  • Explain why Object.freeze doesn't stop a nested object's fields from changing.
  • Use a JSON replacer or reviver to solve a concrete problem — dropping a field on the way out, reviving a Date on the way in.

04Practice this chapter

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

  • 8 testsgroupBy with reduceintermediate
  • 9 testssort() without wrecking the originalintermediate
  • 11 testsSet to dedupe, Map to countintermediate
  • 12 testsGroup with Object.groupBy, compare with Setintermediate
All 33 for this level →
I2

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. I1Previous · I1Scope & functions, properly
  2. I2you are here
  3. Next · I3Prototypes & OOPclass is real syntax now — but it's still prototypes underneath, every time.7 min read

01On this page

15 sections · 0%

  1. 01Getters and setters
  2. 02Object statics — the whole-object toolkit
  3. 03Copying — shallow, deep, and what actually does which
  4. 04Map and Set — objects and arrays with better rules
  5. 05WeakMap and WeakSet
  6. 06Array methods the beginner chapter skipped
  7. 07The immutable array twins — toSorted, toReversed, toSpliced, with
  8. 08findLast and findLastIndex
  9. 09Object.groupBy — grouping without a reduce
  10. 10Set gets algebra: union, intersection, difference
  11. 11Locking an object down: seal, freeze, preventExtensions
  12. 12Sort stability
  13. 13JSON — replacer and reviver
  14. 14Nested destructuring and defaults, past the basics
  15. 15Optional chaining meets deep data

[] chapters · t top