Object.assign Polyfill
Implement Object.assign from scratch: copy own enumerable properties (including Symbols) from sources onto the target.
JavaScript · Objects · Polyfills — Pratik Rai ·
Implement a function that recursively traverses an object, evaluates functions with given arguments, and transforms keys to lowercase. Master recursive object traversal and type checking.
Walking a nested object — to flatten its keys, resolve its values, redact its secrets or diff it against another — is a task that appears constantly in real code and almost as often in interviews. The traversal is straightforward. What makes it interesting is the list of things that are technically objects and will break a naive implementation.
JSfile.javascript1function walk(obj) { 2 for (const key in obj) { 3 if (typeof obj[key] === 'object') { 4 walk(obj[key]); 5 } else { 6 console.log(key, obj[key]); 7 } 8 } 9}
Four distinct bugs live in those six lines.
typeof null === 'object'. The oldest wart in the language. A null value recurses, and for...in over null does nothing, so the value is silently dropped rather than reported.
Arrays are objects. typeof [] === 'object', so arrays get walked as though their indices were meaningful keys. Sometimes that is what you want; it should be a decision, not an accident.
for...in walks the prototype chain. It enumerates inherited enumerable properties too. On a plain object literal that is harmless, but on a class instance or anything with a modified prototype you get keys you never set. Object.keys or Object.entries covers own properties only.
Circular references hang forever. obj.self = obj produces infinite recursion and a stack overflow.
JSfile.javascript1function isPlainObject(value) { 2 return ( 3 typeof value === 'object' && 4 value !== null && 5 !Array.isArray(value) && 6 !(value instanceof Date) && 7 !(value instanceof RegExp) && 8 !(value instanceof Map) && 9 !(value instanceof Set) 10 ); 11} 12 13function flattenObject(input, { delimiter = '.' } = {}) { 14 const result = {}; 15 const seen = new WeakSet(); 16 17 function walk(value, path) { 18 if (!isPlainObject(value)) { 19 result[path] = value; 20 return; 21 } 22 if (seen.has(value)) { 23 result[path] = '[Circular]'; 24 return; 25 } 26 seen.add(value); 27 28 const entries = Object.entries(value); 29 if (entries.length === 0) { 30 result[path] = {}; // preserve empty objects rather than losing them 31 return; 32 } 33 34 for (const [key, child] of entries) { 35 walk(child, path ? `${path}${delimiter}${key}` : key); 36 } 37 } 38 39 walk(input, ''); 40 return result; 41}
JSfile.javascript1flattenObject({ user: { name: 'Ada', meta: { active: true } }, id: 1 }); 2// { 'user.name': 'Ada', 'user.meta.active': true, id: 1 }
The WeakSet is the right structure for cycle detection: it holds object references without preventing garbage collection, and membership testing is O(1). A plain array with includes would be O(n) per check and would keep every visited object alive.
The empty-object branch matters more than it looks. Without it, {a: {}} produces {} — the key disappears entirely, because there are no leaves beneath it to record.
This is the list worth memorising, because each one silently misbehaves under a naive typeof check:
| Value | typeof | Naive traversal does |
|---|---|---|
null | 'object' | recurses into nothing, drops the value |
[] | 'object' | walks indices as keys |
new Date() | 'object' | flattens to {} — no own enumerable keys |
new Map() | 'object' | flattens to {} — entries are internal |
/regex/ | 'object' | flattens to {} |
() => {} | 'function' | usually skipped, sometimes wanted |
Date, Map, Set and RegExp are the quiet ones. They have no own enumerable properties, so they do not throw — they just vanish into empty objects, and you find out when a timestamp turns into {} in production.
There is no universally correct answer, only a decision to make explicitly:
JSfile.javascript1// Keep arrays intact 2{ tags: ['a', 'b'] } → { tags: ['a', 'b'] } 3 4// Index into them 5{ tags: ['a', 'b'] } → { 'tags.0': 'a', 'tags.1': 'b' } 6 7// Bracket notation, as query-string libraries do 8{ tags: ['a', 'b'] } → { 'tags[0]': 'a', 'tags[1]': 'b' }
Form libraries index. Config loaders usually keep arrays whole. Pick one, document it, and make it an option.
Flattening config so environment variables can override nested keys (DATABASE__HOST → database.host).
Redacting secrets before logging — the same traversal, replacing values whose key matches /password|token|secret/i.
Deep equality and diffing — walk both objects in step and compare leaves.
Form state — react-hook-form and similar use dotted paths as field names, which is exactly this transformation.
If you are building the inverse — turning 'a.b.c' back into a nested object — treat the key path as untrusted. A path of __proto__.isAdmin in a naive unflatten writes to Object.prototype and affects every object in the program. This is a real, exploited vulnerability class:
JSfile.javascript1const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']); 2if (FORBIDDEN.has(key)) continue;
Building the result with Object.create(null) removes the risk entirely, since there is no prototype to pollute.
typeof null === 'object' — check for null before recursing, always.Object.entries, not for...in, to avoid inherited keys.Date, Map, Set and RegExp flatten to {} unless you exclude them explicitly.WeakSet to survive circular references.__proto__ and constructor keys.Goal: Implement the recursive object evaluation function within 30-45 minutes. Focus on understanding recursive traversal and type checking.
Continue learning with these related challenges
Implement Object.assign from scratch: copy own enumerable properties (including Symbols) from sources onto the target.
JavaScript · Objects · Polyfills — Pratik Rai ·
Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.
JavaScript · Objects · Prototypes — Pratik Rai ·
Compare two values structurally, because === only ever compares references.
JavaScript · ES6 — Pratik Rai ·
Implement Object.assign from scratch: copy own enumerable properties (including Symbols) from sources onto the target.
JavaScript · Objects · Polyfills
Pratik Rai ·
Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.
JavaScript · Objects · Prototypes
Pratik Rai ·
Compare two values structurally, because === only ever compares references.
JavaScript · ES6
Pratik Rai ·