Object.is
beginnerThe equality that fixes === for NaN and -0: tiny implementation, classic trivia follow-ups.
Implement Object.assign in JavaScript: enumerable own properties, symbol keys, getter/setter semantics, and how it differs from object spread.
Implement
assign(target, ...sources)matchingObject.assign: copy every enumerable own property (string and symbol keys) from each source onto the target, mutate and return the target, and throw if the target isnull/undefined.
The words in bold are the question. Interviewers use this one to check whether you know the property-model vocabulary — own vs inherited, enumerable vs not, string vs symbol keys — and the getter/setter semantics hiding inside an innocent-looking to[key] = from[key].
Object.assign({}, defaults, userOptions) (largely replaced by spread, but ubiquitous in older code).Object.assign(state, patch) keeps the reference stable.Object.assign({}, state, { count: 1 }) was the pre-spread idiom.function assign(target, ...sources) {
if (target == null) {
throw new TypeError("Cannot convert undefined or null to object");
}
// Primitives are boxed: assign("abc", ...) works on a String wrapper.
const to = Object(target);
for (const source of sources) {
if (source == null) continue; // null/undefined sources are skipped
// Reflect.ownKeys = string keys + symbol keys, own only
for (const key of Reflect.ownKeys(source)) {
// Copy only enumerable properties (skips e.g. class methods’ nature
// is irrelevant here — what matters is the enumerable flag)
if (Object.prototype.propertyIsEnumerable.call(source, key)) {
to[key] = source[key]; // Get on source, Set on target — see below
}
}
}
return to;
}Without Reflect.ownKeys, the equivalent is Object.keys(source) plus Object.getOwnPropertySymbols(source) filtered by propertyIsEnumerable — knowing both spellings is useful.
to[key] = source[key] is not a "copy" at the spec level — it's a Get followed by a Set, and both can run user code:
const source = {
get computed() { return Date.now(); }, // getter is EVALUATED...
};
const copy = Object.assign({}, source);
// ...so copy.computed is a plain data property holding a number.
// The getter itself is NOT copied.
const target = {
set value(v) { console.log("setter ran:", v); },
};
Object.assign(target, { value: 42 }); // logs "setter ran: 42"
// target still has the setter; no data property was created.This is the #1 discriminating follow-up: Object.assign copies values, not property descriptors. If you need to clone getters/setters as getters/setters, the tool is Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) — naming that API is a senior tell.
const target = { a: 1 };
const result = assign(target, { b: 2 }, { c: 3, a: 4 });
result; // { a: 4, b: 2, c: 3 } — later sources win
result === target; // true — assign MUTATES and returns the target
assign({}, { a: 1 }, null, undefined, { b: 2 }); // { a: 1, b: 2 } — nullish sources skipped
assign(null, { a: 1 }); // TypeError — only a nullish TARGET throws
const s = assign("ab", { x: 1 });
// String wrapper { 0: 'a', 1: 'b', x: 1 } — primitives box, they don't throw.
// (In strict mode, writing to the read-only indices of a frozen-ish wrapper
// throws — assign onto string primitives is a trivia edge, not a real use.)const original = { nested: { count: 1 } };
const copy = Object.assign({}, original);
copy.nested.count = 99;
original.nested.count; // 99 — the nested OBJECT REFERENCE was copied, not the objectA common interview trap is "extend your assign to deep-merge." Treat that as a different question with real design decisions — how to merge arrays, what to do with class instances, cycles — not a recursive one-liner. See deep clone and structuredClone vs deep clone for the honest treatment.
Object.assign(t, s) | { ...t, ...s } | |
|---|---|---|
| Mutates an existing object | yes — that's its point | no — always a new object |
| Triggers setters on the target | yes | no (spread defines properties directly) |
| Evaluates getters on sources | yes | yes |
| Copies symbol keys | yes | yes |
| Throws on nullish target | yes | { ...null } is {} — no throw |
The setter row is the one that changes behavior in real code — spreading onto a fresh object can't run target-side setters, Object.assign onto a live object can.
Object.keys misses them; Reflect.ownKeys or getOwnPropertySymbols is required.Object.defineProperty(obj, k, { enumerable: false }) don't copy.assign copies left-to-right and stops at the throw; earlier properties are already applied. Object.assign is not transactional. Worth stating unprompted.TypeError at the first write (again: partial application, not a rollback).own keys only. for...in without a hasOwnProperty guard would get this wrong, which is why the modern iteration APIs are the better tool.for...in without an own-property check (walks the prototype chain).assign({}, source[key]) as "deep merge" — it replaces nested objects rather than merging them, and turns arrays into plain objects. If you claim deep behavior, you must handle arrays, cycles, and non-plain objects.Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); this pair exists precisely because Object.assign can't.Object.assign mutate instead of returning a fresh object?" — it's a merge primitive from 2015 designed for the mutation use case; spread covers the immutable case syntactically.Object.assign(obj, obj)?" — a self-assign: every enumerable own property is read and rewritten; getters run, setters run. Harmless for data objects, observable for accessors.pick(obj, keys) using these primitives." — a natural warm-down that reuses the enumerable/own vocabulary.assign is the canonical shallow copy; the deep version is its own question: custom deep clone.The equality that fixes === for NaN and -0: tiny implementation, classic trivia follow-ups.
Walk the prototype chain by hand, then meet Symbol.hasInstance — and learn where instanceof lies to you.
Reimplement typeof with Object.prototype.toString and learn why typeof null === 'object'.
Implement myNew(Constructor, ...args): prototype linking, this binding, and the return-object override rule.