General Rule: Writes Stay Local

let animal = {
    eats: true,
    name: "Generic"
};
 
let rabbit = {
    __proto__: animal
};
 
// WRITE - stays in rabbit, doesn't touch animal
rabbit.eats = false;
rabbit.name = "Bunny";
 
console.log(rabbit.eats); // false (rabbit's own property)
console.log(animal.eats); // true (animal unchanged)

Exception: Getter/Setter Properties

When the prototype has a setter, writes DO go up the chain:

let user = {
    _name: "John",
    
    get name() {
        return this._name;
    },
    
    set name(value) {
        this._name = value.toUpperCase();
    }
};
 
let admin = {
    isAdmin: true,
    __proto__: user
};
 
// This triggers the SETTER in user prototype!
admin.name = "alice";
 
console.log(admin.name); // "ALICE" 
console.log(admin._name); // "ALICE" (set on admin, not user)
console.log(user._name); // "John" (user unchanged)

What Happens in Your Tutorial Example:

Looking at your code:

javascript

const p1 = {
    lname: "Garg",
    getFullname() {
        return `${this.fname} ${this.lname}`;
    }
};
 
const p2 = Object.create(p1);
p2.__proto__.fname = "Hack";  // Direct prototype modification!

This is DIRECTLY modifying the prototype object itself!

// This modifies p1 directly (the prototype object)
p2.__proto__.fname = "Hack";
 
// Same as writing:
p1.fname = "Hack";
 
console.log(p1.fname); // "Hack" (p1 was modified)
console.log(p2.fname); // "Hack" (inherited from p1)

Different Ways to Write:

1. Normal property write (stays local):

javascript

let animal = { eats: true };
let rabbit = { __proto__: animal };
 
rabbit.eats = false; // Creates rabbit.eats, doesn't touch animal

2. Direct prototype modification:

javascript

let animal = { eats: true };
let rabbit = { __proto__: animal };
 
rabbit.__proto__.eats = false; // Modifies animal directly!
// Same as: animal.eats = false;

3. Setter in prototype (special case):

javascript

let user = {
    set fullName(value) {
        [this.firstName, this.lastName] = value.split(" ");
    }
};
 
let admin = { __proto__: user };
 
admin.fullName = "John Doe"; // Calls setter, but this = admin
console.log(admin.firstName); // "John" (set on admin)

Visual Comparison:

javascript

let parent = { x: 1 };
let child = { __proto__: parent };
 
// Case 1: Normal write (stays local)
child.x = 2;
console.log(child.x);  // 2 (child's own property)
console.log(parent.x); // 1 (parent unchanged)
 
// Case 2: Direct prototype write (modifies parent)
child.__proto__.x = 3;
console.log(child.x);  // 3 (now reads from parent)
console.log(parent.x); // 3 (parent was modified)

The Tutorial’s Pattern Explained:

javascript

const p1 = { lname: "Garg" };
const p2 = Object.create(p1);
 
// Method 1: Write to p2 (stays local)
p2.fname = "Local"; // Only p2 gets fname
 
// Method 2: Write to prototype (modifies p1)
p2.__proto__.fname = "Shared"; // p1 gets fname, p2 inherits it

When Writes Go Up the Chain:

✅ Writes go to prototype when:

  • You explicitly write to obj.__proto__.property
  • The prototype has a setter for that property

❌ Writes stay local when:

  • Normal property assignment: obj.property = value
  • Even if prototype has the same property name

Practical Example:

javascript

let animal = {
    species: "unknown",
    
    set species(value) {
        this._species = value.toUpperCase();
    },
    
    get species() {
        return this._species;
    }
};
 
let rabbit = { __proto__: animal };
 
// Normal write - stays local
rabbit.age = 2; 
 
// Setter write - calls prototype setter but this = rabbit
rabbit.species = "rabbit";
 
console.log(rabbit._species); // "RABBIT" (set on rabbit)
console.log(animal._species); // "unknown" (animal unchanged)
 
// Direct prototype write - modifies animal
rabbit.__proto__.color = "brown";
console.log(animal.color); // "brown" (animal was modified)

Bottom line: Normal writes (obj.prop = val) stay local. Only setter properties or direct prototype access (obj.__proto__.prop = val) can affect the prototype chain.