This post explains a quiz originally shared as a LinkedIn poll.
🔹 The Question
function User(name) {
this.name = name;
}
User.prototype.settings = { theme: 'light', notifications: true };
const alice = new User('Alice');
const bob = new User('Bob');
alice.settings.theme = 'dark';
console.log(bob.settings.theme);
console.log(alice.hasOwnProperty('settings'));
console.log(alice.settings === bob.settings);
Hint: When you write alice.settings.theme = 'dark', ask yourself: are you creating a new property on alice, or reaching through the prototype chain to mutate something shared?
Follow me for JavaScript puzzles and weekly curations of developer talks & insights at Talk::Overflow: https://talkoverflow.substack.com/
🔹 Solution
Correct answer: A) dark, false, true
The output is:
dark
false
true
🧠 How this works
JavaScript's prototype chain has an asymmetry that trips up even experienced developers: reading a property walks up the prototype chain, but writing a property creates it directly on the instance — unless the write is a nested property access on a reference type.
When you write alice.settings.theme = 'dark', JavaScript first reads alice.settings. Since alice has no own settings property, the engine walks up the prototype chain and finds the settings object on User.prototype. It returns a reference to that shared object. Then it sets .theme = 'dark' on that shared object — mutating the prototype's settings in place.
This is fundamentally different from alice.settings = { theme: 'dark' }, which would create a new own property on alice via the prototype chain's write semantics. The distinction is between a simple assignment (which shadows on the instance) and a nested property mutation (which reads the prototype reference and mutates through it).
Because alice.settings and bob.settings both resolve to the exact same object on User.prototype, mutating it through one instance affects all instances.
🔍 Line-by-line explanation
function User(name) { this.name = name; }— A constructor function. When called withnew, it creates an instance and sets thenameproperty directly on it.User.prototype.settings = { theme: 'light', notifications: true };— A singlesettingsobject is placed on the prototype. Every instance created fromUserwill share this exact object reference unless they shadow it with an own property.const alice = new User('Alice')— Creates a new instance.alicehas one own property:name: 'Alice'. It has no ownsettingsproperty — it inheritssettingsfromUser.prototype.const bob = new User('Bob')— Same structure.bobinherits the samesettingsobject from the same prototype.-
alice.settings.theme = 'dark'— This is the critical line. JavaScript evaluates this as two operations:-
Read
alice.settings:alicehas no ownsettings, so the engine walks the prototype chain and returnsUser.prototype.settings— the shared object. -
Write
.theme = 'dark': Sets thethemeproperty on the object that was returned. This mutatesUser.prototype.settingsdirectly. No new property is created onalice.
-
Read
console.log(bob.settings.theme)—bob.settingsalso resolves toUser.prototype.settings, which was just mutated. Printsdark.console.log(alice.hasOwnProperty('settings'))—alicenever received an ownsettingsproperty. The mutation went through the prototype reference, not via assignment toalice.settings. Printsfalse.console.log(alice.settings === bob.settings)— Both resolve to the sameUser.prototype.settingsobject. Printstrue.
The non-obvious part: alice.settings.theme = 'dark' looks like it's modifying Alice's settings, but it's actually modifying everyone's settings. The dot-chain reads the shared prototype reference before performing the write, so the mutation leaks across all instances. This is the difference between obj.prop = value (shadows on instance) and obj.prop.nested = value (mutates through prototype).
🔹 Real-World Impact
Where this appears:
Shared default configuration objects: When developers put default config objects on a prototype (or a class's static property) and later mutate nested values, they accidentally change the defaults for all existing and future instances. This is especially common in plugin architectures where a base class provides default options.
Component state in legacy frameworks: Before modern frameworks enforced immutability patterns, it was common to put default state objects on prototypes. Mutating nested state on one component instance would silently corrupt all sibling components sharing the same prototype.
ORM/model defaults: Data model classes with default nested objects (like
permissions: { read: true, write: false }) on the prototype will share state between model instances if any code mutates a nested field instead of replacing the whole object.Mixin patterns: When using
Object.assignor manual prototype extension to mix in behavior with default option objects, any nested mutation on one instance affects all instances that share the mixin.
🔹 Key Takeaways
Never put mutable reference types (objects, arrays) on a prototype. Primitive values on prototypes are safe because assignments always shadow them on the instance. Objects and arrays are shared references, and nested mutations affect all instances.
obj.prop.nested = valuereads through the prototype chain, then mutates the shared reference. Only a direct assignment likeobj.prop = valuecreates an own property on the instance and shadows the prototype.hasOwnPropertyis your diagnostic tool. Ifinstance.hasOwnProperty('prop')returnsfalse, you're reading from the prototype — and any nested mutation will affect all instances.Initialize mutable defaults in the constructor, not on the prototype. Use
this.settings = { ...User.defaults }orthis.settings = Object.assign({}, defaultSettings)inside the constructor to give each instance its own copy.Modern class syntax has the same trap. Writing
class User { settings = { theme: 'light' } }is safe (class fields create own properties), butUser.prototype.settings = { ... }after the class definition is not. Know the difference.
Top comments (0)