A JavaScript constructor sets `Ctor.prototype.tags = []`. One instance runs `a.tags.push('x')` and another runs `b.tags = ['y']`. Explain why the two operations have completely different effects.
answer
- one array, many referrers
- push is a read then a mutation
- assignment shadows, mutation does not
- hasOwn tells you who really owns it
- mutable state belongs on the instance
basics
~20 sBoth instances inherit one array object from the prototype, so push mutates that single shared array and every instance sees the change. Assignment does not touch the prototype: it creates an own tags property on that one instance, which then stops sharing.
solid answer
~50 sThe prototype holds a single array object, and every instance inherits a reference to that same object. `a.tags` is a read, so it walks the chain and returns the shared array; `push` then mutates it in place, and because `b` has no own `tags`, `b.tags` reads that same mutated array. `b.tags = ['y']` is a write, so it creates an own `tags` property on `b` pointing at a brand-new array — the prototype's array is untouched, and `b` now shadows it. So mutation leaks across instances while assignment silently gives one instance private state. The fix is to keep mutable per-instance state off the prototype: assign `this.tags = []` in the constructor, or declare a class field, both of which create a fresh array per instance. Prototypes are for shared behaviour, not shared mutable data.
code
javascript · 18 linesfunction Post() {}
Post.prototype.tags = [];
const a = new Post();
const b = new Post();
a.tags.push('x');
console.log(b.tags); // ['x'] — shared array mutated
console.log(Object.hasOwn(a, 'tags')); // false
b.tags = ['y']; // creates an own property on b
console.log(b.tags); // ['y']
console.log(a.tags); // ['x'] — prototype array untouched
function FixedPost() {
this.tags = []; // fresh array per instance
}
console.log(new FixedPost().tags === new FixedPost().tags); // falsego deeper
Recognise that an array put on a prototype exists once and is seen by every instance, and that per-instance data should be created inside the constructor.
Walk the two operations apart: the read resolves up the chain to one shared array which push then mutates, while the assignment creates an own property that shadows it. Show the fix and why it works.
Connect it to the symptoms you would actually see — cross-request or cross-test bleed, state surviving between instances — and describe how you would confirm it with an ownership and identity check before changing code.
Own the guideline: prototypes carry behaviour, instances carry state, and any mutable default on a shared object is a latent aliasing bug. Note that the same trap recurs wherever defaults are shared, so the answer is a convention, not a one-off fix.
## The setup ```js function Post() {} Post.prototype.tags = []; const a = new Post(); const b = new Post(); ``` One array object exists. Neither `a` nor `b` has an own `tags`; both merely have a prototype link to `Post.prototype`, which owns the array. ## Why push leaks `a.tags.push('x')` is two separate operations. First `a.tags` is *read*: no own property, so the engine walks to `Post.prototype`, finds `tags`, and returns the array reference. Then `push` is called on that array, mutating it in place. No property was assigned anywhere, so nothing about `a` changed at all. Since `b` also has no own `tags`, `b.tags` performs the same walk and lands on the same array object: ```js a.tags.push('x'); b.tags; // ['x'] — same object Object.hasOwn(a, 'tags'); // false ``` Every instance ever created, including instances created before the push, sees `['x']` — the chain is a live lookup path, not a snapshot. ## Why assignment does not leak `b.tags = ['y']` is a *write*. The ordinary write path creates an own data property on the receiver: ```js b.tags = ['y']; Object.hasOwn(b, 'tags'); // true b.tags; // ['y'] a.tags; // ['x'] — prototype array, unchanged ``` `b` now shadows the inherited `tags`. From this moment `b` is isolated and `a` is not, which is exactly what makes the bug so confusing in a real codebase: some instances have quietly opted out of sharing and others have not, depending purely on which code path ran first. ## The symptom in the wild The report is usually shaped like "items from one user show up in another user's list", or "my test passes alone and fails in the suite". Both come from state that outlives the instance because it never belonged to the instance. Primitives on a prototype hide the problem, because any change to a primitive is written with `=` and therefore shadows — `this.count++` reads the inherited 0 and then *assigns* 1 as an own property. Objects and arrays expose it, because they are usually mutated rather than reassigned. ## Diagnosing it Two checks settle it immediately: ```js Object.hasOwn(instance, 'tags'); // false ⇒ it is inherited Object.getPrototypeOf(a).tags === b.tags; // true ⇒ they share one object ``` If the second is true for two supposedly independent instances, you have shared mutable prototype state. ## The fix Give each instance its own object. In a constructor function or a class constructor: ```js function Post() { this.tags = []; // own property, fresh array per instance } ``` Or with class syntax, where an instance field is created per instance rather than on the prototype: ```js class Post { tags = []; } ``` Both run once per construction, so every instance gets a distinct array. ## What still belongs on the prototype Methods. A function on the prototype is shared deliberately: one function object serving every instance, invoked with `this` bound to the instance that received the call, so it reads and writes per-instance state without holding any. Frozen constants are also safe, because there is no mutation to leak. The rule of thumb is that anything you will ever mutate belongs on the instance; anything you only ever call or read belongs on the prototype. ## The deeper point This is not a quirk of arrays. It is the read/write asymmetry made visible: reading gives you a reference into shared storage, while writing creates private storage on the receiver. Once you internalise that, the behaviour of every variant — nested objects, `Map` instances, default option bags on a prototype — follows without memorisation.
- Why does the same bug usually stay hidden when the prototype property is a number rather than an array?Because numbers are almost always updated by assignment. `this.count++` reads the inherited value and then assigns the result, which creates an own property on that instance — so it shadows on first use and never leaks. Arrays and objects are mutated in place instead, which touches the one shared object.
- How would you confirm at runtime that two instances are sharing one object rather than each holding their own?Compare identity and ownership: `a.tags === b.tags` being true with `Object.hasOwn(a, 'tags')` false proves both reads resolved to the same object on the prototype. If each instance had its own, the identity check would be false and `hasOwn` would be true on both.
- Is there any state that is legitimately safe to put on a prototype?Yes — anything that is never mutated: methods, which are the whole point of the prototype, plus immutable constants such as frozen configuration objects, strings or numbers used as defaults. The hazard is exclusively mutable shared objects, because mutation bypasses the shadowing that assignment would have produced.
saying these in an interview costs you the question
- Says push creates an own property on the instance
- Thinks each instance gets a copy of the prototype's array
- Claims assignment and mutation behave the same here
- Suggests freezing the instance instead of moving the state
- Says only instances created after the push are affected