Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

JavaScript Objects, Destructuring, and Spread: Immutable Updates Done Right

Objects are JavaScriptโ€™s universal container โ€” every config, API payload, state blob, and options bag is one. Modern JS layered a syntax toolkit on top of them (destructuring, spread, shorthand, optional chaining) that changed how the language looks: code written since ~2018 is dense with { ...state, name } and const { data } = res in ways older tutorials never mention.

This guide covers that toolkit properly โ€” including the shallow-copy trap hiding inside spread, the immutable-update patterns frameworks assume, and the honest answer on when a Map beats a plain object.


Modern Object Syntax in Ninety Seconds

const name = "Ada", role = "admin";
const user = {
name, // shorthand: name: name
role,
[`${role}Since`]: 2024, // computed key: adminSince
greet() { // method shorthand
return `Hi, ${this.name}`;
},
};

Property shorthand ({ name }), computed keys ([expr]:), and method shorthand are pure convenience โ€” but theyโ€™re everywhere, so reading them must be automatic. Two access notes: dot for known keys, brackets for dynamic ones (user[fieldName]); and key order โ€” objects preserve insertion order for string keys (with integer-like keys sorted first โ€” a quirk to know, not to rely on).

Property existence versus value is a distinction worth keeping crisp: "role" in user and Object.hasOwn(user, "role") test presence; user.role !== undefined conflates โ€œabsentโ€ with โ€œpresent but undefined.โ€ The iteration workhorses:

Object.keys(user) // ["name", "role", "adminSince", "greet"]
Object.values(user)
Object.entries(user) // [["name","Ada"], ...] โ€” pairs โ†’ destructure in loops:
for (const [key, value] of Object.entries(user)) { ... }
Object.fromEntries(pairs) // the inverse โ€” build objects from pairs

entries/fromEntries bracket a useful pattern: transform an object by round-tripping through pairs and array methods โ€” Object.fromEntries(Object.entries(obj).filter(...)).


Destructuring: Unpacking as Syntax

Destructuring pulls values out of objects/arrays in the shape you declare:

const res = { status: 200, data: { id: 7, name: "Ada" }, meta: null };
const { status, data } = res; // two consts in one line
const { data: { name } } = res; // nested reach-in
const { missing = "fallback" } = res; // default (fires on undefined only)
const { status: code } = res; // rename: code = 200
const { status: s2, ...rest } = res; // rest: everything else, new object
// arrays destructure by position
const [first, second] = [10, 20, 30];
const [head, ...tail] = list;
const [, , third] = list; // skip with holes
const [a, b] = [b, a]; // the classic swap
// and it composes with iteration and returns
for (const [k, v] of Object.entries(config)) { ... }
const [x, y] = getCoordinates(); // multi-value returns, tuple-style

The highest-value application is function parameters โ€” the options-object pattern from the functions guide, which destructuring makes ergonomic:

function paginate({ page = 1, size = 20, sort = "id" } = {}) { ... }
paginate({ size: 50 }); // named, order-free, defaulted
paginate(); // works too โ€” note the outer = {} guard

That trailing = {} matters: without it, calling paginate() destructures undefined and throws. With it, everything defaults. This exact signature shape is modern JS API design in one line.

Two cautions: deep destructuring of possibly-missing branches throws (const { data: { name } } = res explodes if data is null โ€” guard with defaults or optional chaining first), and heavily nested destructures can obscure more than they reveal. Two levels is a sensible ceiling.


Spread and the Shallow-Copy Trap

Spread (...) expands an objectโ€™s (or arrayโ€™s) entries into a new literal:

const defaults = { theme: "light", lang: "en", retries: 3 };
const userPrefs = { lang: "hi" };
const config = { ...defaults, ...userPrefs }; // merge โ€” LATER WINS
// { theme: "light", lang: "hi", retries: 3 }
const updated = { ...config, retries: 5 }; // "change" one field, new object

Later-wins ordering makes spread the standard tool for defaults-then-overrides merging and for immutable updates. But the fine print is the section title: spread copies one level deep. Nested objects are copied by reference โ€” shared, not duplicated:

const state = { user: { name: "Ada" }, tags: ["a"] };
const copy = { ...state };
copy.user.name = "Grace"; // ALSO changes state.user.name โ€” same inner object!
copy.tags.push("b"); // ALSO visible via state.tags
copy.user = { name: "Alan" }; // this is fine โ€” replacing, not mutating

The rule that resolves it: to update a nested field immutably, spread every level on the path:

const next = {
...state,
user: { ...state.user, name: "Grace" }, // new outer, new user, shared rest
};

Verbose? Yes โ€” deliberately: the verbosity is the path being copied. This nested-spread idiom, plus the array equivalents (map-with-conditional-spread from the array guide), is the backbone of state updates in React/Redux-style code. When the nesting gets truly deep, thatโ€™s a signal to flatten your state shape โ€” or reach for a genuine deep copy:

const clone = structuredClone(state); // real deep copy โ€” modern, handles Dates,
clone.user.name = "Grace"; // Maps, Sets, cycles; not functions/DOM nodes

structuredClone retired the JSON.parse(JSON.stringify(x)) hack (which silently dropped undefined, Dates-as-objects, Maps, and functions). Deep-copy sparingly, though โ€” itโ€™s O(everything); the spread-the-path idiom shares all unchanged branches, which is both faster and what makes reference-equality change detection work.

Why does the ecosystem insist on immutability at all? The same reasons as in Java โ€” local reasoning, safe sharing โ€” plus one JS-specific superpower: frameworks detect changes by cheap reference comparison. New object = changed; same object = skip. Mutation breaks that contract invisibly.


Optional Chaining and Friends: Navigating Uncertain Shapes

API data is uncertain by nature; ES2020โ€™s operators made navigating it civilized:

const city = order?.customer?.address?.city ?? "unknown";
const first = order?.items?.[0]; // optional index
const label = order?.format?.(); // optional call

?. short-circuits to undefined at the first null/undefined; ?? supplies the fallback without eating legitimate falsy values. Together they replaced pyramids of x && x.y && x.y.z guards. Related assignment shorthands: x ??= v (assign if nullish โ€” lazy defaults), plus ||=/&&=.

One discipline: optional chaining tolerates absence โ€” it doesnโ€™t explain it. At trust boundaries (API responses), prefer validating the shape once (schema check) over sprinkling ?. through fifty call sites; a payload thatโ€™s wrong should fail loudly at arrival, not propagate undefined quietly.


Map and Set: When Plain Objects Arenโ€™t the Right Dictionary

Objects moonlight as key-value stores, but Map is the purpose-built one:

const cache = new Map();
cache.set(userObj, computedProfile); // ANY key type โ€” objects, not just strings
cache.get(userObj);
cache.size; // real size, no Object.keys().length
for (const [k, v] of cache) { ... } // directly iterable, insertion order
const seen = new Set();
seen.add(id);
seen.has(id); // O(1) membership โ€” the point of sets

Choose Map when keys are dynamic/unknown (user-generated IDs, object keys), when you add/remove a lot, or when size and iteration matter โ€” it also dodges object pitfalls like the prototype chain ("toString" in obj is true for {}) and key coercion (object keys are stringified: obj[1] and obj["1"] collide; Map distinguishes them). Choose plain objects for fixed, known shapes โ€” records with named fields, i.e., most data โ€” where literal syntax, destructuring, and JSON round-tripping are the daily win (Map doesnโ€™t JSON-serialize without conversion). WeakMap/WeakSet add the no-ownership caching behavior โ€” keys donโ€™t keep objects alive โ€” for metadata-attachment cases.

The honest summary: objects for records, Map for dictionaries. Most โ€œshould I use Map?โ€ hesitancy dissolves under that sentence.


JSON: The Boundary Format

Objects meet the outside world as JSON, and the conversion pair has edges worth knowing before they bite:

const payload = JSON.stringify(order); // object โ†’ string
const parsed = JSON.parse(response); // string โ†’ object

What stringify silently does: drops undefined-valued properties, functions, and symbols entirely ({ a: undefined } โ†’ "{}"); converts Dates to ISO strings (they come back as strings, not Dates โ€” the classic round-trip surprise); turns Map/Set into useless {}; and throws on circular references. What parse never does: revive types โ€” everything returns as plain objects, arrays, strings, numbers, booleans, null.

The practical consequences: define explicit serialization for rich types (a toJSON() method on a class controls its own output), convert Maps via Object.fromEntries(map) before sending, re-hydrate dates deliberately after parsing, and treat parse output as untrusted shape needing validation โ€” JSON.parse guarantees syntax, never structure. Two lesser-known second arguments help in a pinch: JSON.stringify(obj, null, 2) pretty-prints for logging, and a replacer/reviver function pair can transform values in transit โ€” though a schema library does that job more maintainably.

One performance note that surfaces in interviews: parse(stringify(x)) as a deep-copy hack is both lossy (all of the above) and slow versus structuredClone โ€” its persistence in tutorials is pure inertia. You know better now.

Frequently Asked Questions

Does Object.freeze give real immutability? Shallowly โ€” top-level writes are ignored (or throw in strict mode), but nested objects stay mutable. Deep-freezing requires recursion, and the ecosystem mostly skips it: convention (never mutate) plus reference-equality tooling has proven sufficient, with TypeScriptโ€™s readonly catching violations at compile time.

Spread vs Object.assign? { ...a, ...b } and Object.assign({}, a, b) produce the same result; spread reads better and won. Object.assign(target, src) โ€” with an existing target โ€” mutates it; thatโ€™s occasionally wanted, usually not.

How do I remove a key immutably? Destructure-and-rest: const { password, ...safe } = user; โ€” safe is a new object minus password. (The mutating counterpart is delete user.password โ€” same reference caveats as ever.)

Why did my two โ€œidenticalโ€ objects fail ===? Reference equality โ€” different objects, even with equal contents. Compare fields, use a library deepEqual, or restructure to compare identities (a.id === b.id).

Is there a record/tuple with value equality coming? The long-discussed Records & Tuples proposal stalled; as of now, value-equal compound primitives arenโ€™t in the language. Conventions (immutability + id-based identity) and libraries fill the gap.


One More Tool: Getters and Setters on Plain Objects

Object literals (and classes) support computed properties that run code on access โ€” occasionally exactly right:

const cart = {
items: [],
get totalPaise() { // read as cart.totalPaise โ€” no parens
return this.items.reduce((s, i) => s + i.paise, 0);
},
set discount(pct) { // cart.discount = 10 runs validation
if (pct < 0 || pct > 100) throw new RangeError("0-100 only");
this._discount = pct;
},
};

Getters shine for derived values that should always be fresh (totals, full names) โ€” no stale cached field to forget updating. Setters earn their place for validation on assignment. The caution: property access that secretly does heavy work or has side effects violates every readerโ€™s expectations (obj.total looks free; if itโ€™s O(nยฒ), thatโ€™s a trap). Keep getters cheap and pure, prefer explicit methods for anything expensive, and note that JSON.stringify does include getter values while spread copies them as plain data โ€” derived state becomes frozen state the moment itโ€™s copied.

Self-Check

const base = { a: 1, nested: { b: 2 } };
// 1
const c1 = { ...base };
c1.a = 10; c1.nested.b = 20;
console.log(base.a, base.nested.b);
// 2
const { nested: { b }, x = 5 } = base;
console.log(b, x);
// 3
const merged = { a: 9, ...base };
console.log(merged.a);
// 4 โ€” write it: swap keys/values of obj in one line
// 5 โ€” what's wrong: const ids = {}; ids[user1] = 1; ids[user2] = 2;

Answers: (1) 1, 20 โ€” top level copied, nested shared: the shallow trap in four lines. (2) 2, 5. (3) 1 โ€” spread later wins; ...base overwrites the earlier a: 9. (4) Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])). (5) object keys stringify โ€” both users become "[object Object]", one entry, silent collision. Thatโ€™s a Map.

Next: how files became components โ€” ES Modules: import/export semantics, default-vs-named debates, dynamic import, and how bundlers see your dependency graph.