DEV Community

ValPetal Tech Labs
ValPetal Tech Labs

Posted on

Javascript Question of the Day #20 [Talk::Overflow]

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🧠 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

  1. function User(name) { this.name = name; } — A constructor function. When called with new, it creates an instance and sets the name property directly on it.

  2. User.prototype.settings = { theme: 'light', notifications: true }; — A single settings object is placed on the prototype. Every instance created from User will share this exact object reference unless they shadow it with an own property.

  3. const alice = new User('Alice') — Creates a new instance. alice has one own property: name: 'Alice'. It has no own settings property — it inherits settings from User.prototype.

  4. const bob = new User('Bob') — Same structure. bob inherits the same settings object from the same prototype.

  5. alice.settings.theme = 'dark' — This is the critical line. JavaScript evaluates this as two operations:

    • Read alice.settings: alice has no own settings, so the engine walks the prototype chain and returns User.prototype.settings — the shared object.
    • Write .theme = 'dark': Sets the theme property on the object that was returned. This mutates User.prototype.settings directly. No new property is created on alice.
  6. console.log(bob.settings.theme)bob.settings also resolves to User.prototype.settings, which was just mutated. Prints dark.

  7. console.log(alice.hasOwnProperty('settings'))alice never received an own settings property. The mutation went through the prototype reference, not via assignment to alice.settings. Prints false.

  8. console.log(alice.settings === bob.settings) — Both resolve to the same User.prototype.settings object. Prints true.

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.assign or 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

  1. 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.

  2. obj.prop.nested = value reads through the prototype chain, then mutates the shared reference. Only a direct assignment like obj.prop = value creates an own property on the instance and shadows the prototype.

  3. hasOwnProperty is your diagnostic tool. If instance.hasOwnProperty('prop') returns false, you're reading from the prototype — and any nested mutation will affect all instances.

  4. Initialize mutable defaults in the constructor, not on the prototype. Use this.settings = { ...User.defaults } or this.settings = Object.assign({}, defaultSettings) inside the constructor to give each instance its own copy.

  5. Modern class syntax has the same trap. Writing class User { settings = { theme: 'light' } } is safe (class fields create own properties), but User.prototype.settings = { ... } after the class definition is not. Know the difference.

Top comments (0)