Object.create Polyfill
Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.
JavaScript · Objects · Prototypes — Pratik Rai ·
Implement Object.assign from scratch: copy own enumerable properties (including Symbols) from sources onto the target.
Object.assign copies enumerable own properties from one or more source objects onto a target, mutating and returning that target. It is the method behind most "merge these options" code, and its two most important properties — that the copy is shallow and that it triggers setters — are the source of a steady stream of bugs.
JSfile.javascript1Object.assign({ a: 1 }, { b: 2 }, { a: 3 }); // { a: 3, b: 2 }
Sources are applied left to right, so later sources win. The target is mutated in place and returned — it is not a pure function, which is why Object.assign({}, defaults, overrides) with a fresh literal target is the safe idiom.
It copies:
Object.defineProperty with enumerable: false is skipped.This is the single most consequential thing to understand:
JSfile.javascript1const original = { user: { name: 'Ada' }, tags: ['a'] }; 2const copy = Object.assign({}, original); 3 4copy.user.name = 'Grace'; 5original.user.name; // 'Grace' — same object reference
Only the top level is copied. Nested objects and arrays are shared by reference between source and target. Code that "clones" config or state with Object.assign({}, obj) and then mutates a nested field is modifying the original too.
For a genuine deep copy, use structuredClone(obj) — built into every modern runtime, and it handles Date, Map, Set, ArrayBuffer and circular references. The old JSON.parse(JSON.stringify(obj)) trick silently destroys undefined, functions, Date objects (they become strings) and throws on cycles.
Spread syntax has identical semantics: {...obj} is also shallow.
Object.assign uses [[Set]], not [[DefineOwnProperty]]. That means it invokes setters on the target and reads getters on the source:
JSfile.javascript1const target = { 2 set value(v) { console.log('setter ran with', v); }, 3}; 4Object.assign(target, { value: 42 }); // logs "setter ran with 42" 5target.value; // undefined — nothing was stored
Spread behaves differently: {...target, value: 42} defines a plain data property and never calls a setter. That divergence surprises people who assume the two are interchangeable.
It also means getters on the source are evaluated, and their returned values copied. You get the computed result, not the getter itself. Copying an object while preserving its accessors requires Object.getOwnPropertyDescriptors:
JSfile.javascript1Object.defineProperties({}, Object.getOwnPropertyDescriptors(source));
JSfile.javascript1Object.myAssign = function (target, ...sources) { 2 if (target === null || target === undefined) { 3 throw new TypeError('Cannot convert undefined or null to object'); 4 } 5 6 const to = Object(target); 7 8 for (const source of sources) { 9 // null and undefined sources are skipped, not an error. 10 if (source === null || source === undefined) continue; 11 12 const from = Object(source); 13 14 for (const key of Reflect.ownKeys(from)) { 15 const descriptor = Object.getOwnPropertyDescriptor(from, key); 16 if (descriptor && descriptor.enumerable) { 17 to[key] = from[key]; // [[Set]] — triggers setters by design 18 } 19 } 20 } 21 22 return to; 23};
Reflect.ownKeys returns both string and symbol keys, which Object.keys does not. Checking enumerable via the descriptor is what correctly skips non-enumerable properties while still including symbols.
Note the asymmetry in error handling: a null target throws, but null sources are silently skipped. That is deliberate in the spec and makes Object.assign(base, maybeNull, maybeUndefined) safe.
Assignment can throw mid-way. If a target property is read-only, [[Set]] throws in strict mode — after earlier properties have already been copied. Object.assign is not atomic, and a failed call leaves a partially mutated target.
Primitives get boxed. Object.assign({}, 'abc') yields {0:'a', 1:'b', 2:'c'}, since the string is wrapped and its indices are enumerable own properties.
Arrays are objects. Object.assign([1,2,3], [4]) gives [4,2,3] — index 0 overwritten, the rest untouched. Almost never what anyone wants.
Prototypes are not copied. The result is a plain object; class instances lose their identity. Object.assign({}, instance) instanceof MyClass is false.
Object.assign versus spreadPrefer spread for creating new objects — it is clearer and avoids the setter surprise. Reach for Object.assign when you specifically need to mutate an existing object, such as updating a store in place or copying onto a class instance in a constructor. When copying symbol-keyed or accessor properties matters, neither is right; use descriptors.
structuredClone for depth.{} when you want a copy.[[Set]], so target setters fire and source getters are evaluated.null target throws; null sources are skipped.Goal: Implement Object.myAssign with correct symbol support and null-source handling.
Continue learning with these related challenges
Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.
JavaScript · Objects · Prototypes — Pratik Rai ·
Group an array into buckets by a key function — now a real language feature as Object.groupBy.
JavaScript · ES6 — 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.
JavaScript · Recursion · Objects — Pratik Rai ·
Implement Object.create using the classic empty-constructor trick to set an object's prototype chain.
JavaScript · Objects · Prototypes
Pratik Rai ·
Group an array into buckets by a key function — now a real language feature as Object.groupBy.
JavaScript · ES6
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.
JavaScript · Recursion · Objects
Pratik Rai ·