Flatten Object Keys
intermediateTurn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.
Support Python-style arr[-1] in JavaScript with a Proxy — symbol-key crash traps, Reflect forwarding, why Array.prototype.at exists, and honest Proxy trade-offs.
Make
arr[-1]return the last element, Python-style — reads and writes.
Property syntax can't be overloaded in JavaScript, so this is really a Proxy question: intercept property access, translate negative indices, forward everything else untouched. Interviewers use it as the gentlest realistic introduction to get/set traps — and to a crash bug that almost every first attempt contains.
Before the Proxy, the modern context: Array.prototype.at(-1) (ES2022) already does negative reads. The Proxy exercise earns its keep for the bracket syntax, for writes, and as Proxy education. Opening with "the platform answer is .at(-1); here's the from-scratch version" frames you as current, not ignorant of the built-in.
function withNegativeIndexing(arr) {
// Translate "-1"-style STRING keys; pass symbols & non-indices through untouched
const toIndex = (prop) => {
if (typeof prop !== "string") return null; // ← the crash guard, see below
const n = Number(prop);
return Number.isInteger(n) && n < 0 ? String(arr.length + n) : null;
};
return new Proxy(arr, {
get(target, prop, receiver) {
const mapped = toIndex(prop);
return Reflect.get(target, mapped ?? prop, receiver);
},
set(target, prop, value, receiver) {
const mapped = toIndex(prop);
return Reflect.set(target, mapped ?? prop, value, receiver);
},
has(target, prop) {
const mapped = toIndex(prop);
return Reflect.has(target, mapped ?? prop);
},
});
}const arr = withNegativeIndexing([10, 20, 30, 40, 50]);
arr[-1]; // 50
arr[-2]; // 40
arr[0]; // 10 — non-negative access untouched
arr[-3] = 100; // writes to index 2
arr[2]; // 100
-1 in arr; // true — the has trap
arr.length; // 5 — string prop "length" passes through
[...arr]; // [10,20,100,40,50] — iteration works (symbols forwarded!)
arr.map((x) => x) // works — methods read via the SAME get trapThe naive version starts every trap with const index = Number(prop) — and a Proxy's prop is a string or a symbol. Number(someSymbol) throws a TypeError. So the moment anything touches a well-known symbol — which is immediately:
const broken = naiveNegativeArray([1, 2, 3]);
[...broken]; // 💥 TypeError — spread reads Symbol.iterator
for (const x of broken) {} // 💥 same
JSON.stringify(broken); // 💥 toJSON/Symbol probing, depending on engine pathIteration, spread, for...of, many built-ins — all crash. The guard is one line (typeof prop !== "string" → forward untouched), and it's the line this question exists to check. If you remember one thing: Proxy traps see symbols, and symbols don't coerce to numbers.
Reflect, not target[prop]Reflect.get(target, prop, receiver) is the default behavior as a function — forwarding through it means everything you didn't intend to change (prototype getters, receiver binding, exotic array behavior like length updates on write) keeps working exactly as before. Hand-rolling target[prop] happens to work here for the simple cases but breaks this-sensitive accessors — and "traps should delegate to Reflect for everything they don't explicitly change" is the Proxy best practice worth stating as a rule.
Also note what we did not trap: deleteProperty, ownKeys, getOwnPropertyDescriptor… Un-trapped operations fall through to the target automatically. Minimal traps = minimal surface for inconsistency (e.g., our has says -1 in arr is true, but Object.keys still won't list -1 — a coherent choice because we left ownKeys alone, and being able to defend that coherence is senior-grade Proxy literacy).
arr[-99] maps to a negative real index → undefined on read (fine); on write it creates a bizarre "-94" string key. Policy fork: allow (garbage in, garbage out), or throw a RangeError — either is defensible stated; silently swallowing the write (as many "enhanced" versions do) is the worst option because assignments appear to succeed and vanish.length changes between accesses — translation reads arr.length at access time, so -1 always means "current last" — the right semantics, worth saying explicitly."-0" — Number("-0") is -0, Number.isInteger(-0) is true, -0 < 0 is false → passes through untouched. Accidental correctness; tracing it shows rigor (the -0 story).arr[-1.5] isn't an index; falls through as an ordinary (weird) property, matching how real arrays treat arr[1.5].Not for negative indexing — for the pattern: intercept property access to add behavior without changing call sites. Vue 3's reactivity (get records dependencies, set triggers re-renders), Immer's draft objects (which you build in the Redux+Immer question), validation shells, API clients where client.users.get() synthesizes routes from property names, and test spies. The cost side: every trapped access is a function call — engines can't optimize proxied hot paths nearly as well, which is why Vue documents perf caveats and why nobody ships negative-index arrays for real. Trade-off named, question closed.
Number(prop) before the symbol guard (the crash).Reflect and hand-forwarding — breaks accessors/receiver subtly..at(-1) or the perf cost — reads as clever, not judicious.-1…-n on a fixed-size wrapper — knowing that workaround and its staleness problem shows depth.).at() compare?" — reads only, a method not syntax, zero interception cost, works on strings and typed arrays too. The pragmatic default since ES2022.Symbol.iterator doing in my trap?" — every protocol the language runs (iteration, instanceof via Symbol.hasInstance, string coercion via Symbol.toPrimitive) flows through property reads — the trap sees the language's internals walking by. That realization is the actual education here (symbols beyond uniqueness).get collects the running effect as a subscriber, set notifies — 20 lines for the toy; the event-emitter is the notification half.defineProperty interception?" — defineProperty needs keys up front (Vue 2's array caveats!); Proxy intercepts all keys including future ones — that limitation is literally why Vue rewrote reactivity for v3.Turn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.
Flatten arbitrarily nested arrays recursively and iteratively — then compare with Array.prototype.flat.
Structural equality for nested data: type checks, key comparison, and recursion done right.
Recursively clone nested objects and arrays, handling cycles with a WeakMap.