Search code examples
javascriptinheritanceprototypal-inheritanceproto

Possible error in MDN explanation of __proto__ property?


So, in working to further solidify my understanding of Object-Oriented JavaScript, I have been voraciously reading, and then testing things that I don't understand. I was reading the Mozilla Developer Network (MDN) article titled "Object.prototype.proto" at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

and came across the following explanation:

For objects created using new fun, where fun is a function defined in a script, this value [__proto__] is the value of fun.prototype at the time new fun is evaluated. (That is, if a new value is assigned to fun.prototype, previously-created fun instances will continue to have the previous value as their [[Prototype]], and subsequent new fun calls will use the newly-assigned value as their [[Prototype]].)

Note: MDN is using [[Prototype]] to refer to the "internal" Prototype of an object, which is referenced as __proto__ in JavaScript code.

So I opened up my Chrome console, and wrote some simple JavaScript as such:

function Person(name, age)
{
    this.name = name?name:"Parent Function";
    this.age = age?age:"Old as Time";
}

var parent = new Person("Ebeneezer", 42);    

//new Person evaluated before strength is added to Person.prototype
var child = new Person("Aluiscious", 12);

console.log(child.strength);

Person.prototype.strength = "your value here";
console.log(child.strength);

var second_child = new Person('Sprout', 5);
console.log(second_child.strength);

After this, if I type in child.__proto__ and second_child.__proto__ into the console, I get the same value, which is Person {strength: "your value here"}

According to MDN, shouldn't child.__proto__ "continue to have the previous value" of Person.prototype as their internal Prototype?


Solution

  • The MDN docs are talking about completely replacing the prototype, not adding new properties or methods to it (which will be added to all objects sharing that prototype since the internal [[Prototype]] property is shared). Consider this example:

    function Person(name, age)
    {
        this.name = name?name:"Parent Function";
        this.age = age?age:"Old as Time";
    }
    
    Person.prototype.strength = "some strength";
    var parent = new Person("Ebeneezer", 42);
    
    console.log(parent.strength); //"some strength"
    
    //Replace `Person.prototype` with a completely new prototype object
    Person.prototype = {
        //setting the 'constructor' property correctly when replacing a prototype object
        //is a best practice, but it will work without this too
        constructor: Person
    };
    
    console.log(parent.strength); //still "some strength"
    
    var child = new Person("Aluiscious", 12);
    
    //This will be undefined, because the object was created after the prototype was changed
    console.log(child.strength);
    

    In the above example, the [[Prototype]] properties of the instances refer to two different prototype objects, since I replaced the prototype using .prototype = before creating the second object.

    It's important to understand that the internal prototype property is shared among all instances created with the same prototype. That's why in your example, the strength property gets added to both objects - the internal [[Prototype]] property of both objects is still a reference to the same shared prototype object. It's also important to recognize that object and array properties of the prototype are shared as well. So for example, suppose you added a children array to your Person prototype:

    //Don't do this!
    Person.prototype.children = [];
    var parent1 = new Person("Ebeneezer", 42);
    parent1.children.push(new Person("Child A"));
    
    var parent2 = new Person("Noah", 35);
    parent2.children.push(new Person("Child B"));
    

    You might expect that this would cause Ebeneezer to have an array containing only Child A, and Noah to have an array containing only Child B, but in fact both parents will now have an array containing BOTH Child A and Child B, because children actually refers to the same array belonging to the internal [[Prototype]] object.

    That's why I consider it a best practice to always declare data properties in the constructor, and only methods on the prototype. For example:

    function Person(name, age)
    {
        this.name = name?name:"Parent Function";
        this.age = age?age:"Old as Time";
        this.children = [];
    }
    
    //it's fine to declare methods on the prototype - in fact it's good, because it saves
    //memory, whereas if you defined them in the constructor there would be a separate copy
    //of the method for each instance
    Person.prototype.addChild = function(child) {
        if (!child instanceof Person) {
            throw new Error("child must be a Person object");
        }
        //Note: in a real system you would probably also want to check that the passed child
        //object isn't already in the array
        this.children.push(child);
    }
    

    Note: The modification vs. replacement concept applies to prototype properties in addition to the prototypes themselves. If you set a property directly on an object, it will be used instead of the property on the prototype. So if I were to change my above example to this:

    Person.prototype.children = [];
    var parent1 = new Person("Ebeneezer", 42);
    parent1.children.push(new Person("Child A"));
    
    var parent2 = new Person("Noah", 35);
    parent2.children = [];
    //now `parent2` has its own `children` array, and Javascript will use that
    //instead of the `children` property on the prototype.
    parent2.children.push(new Person("Child B"));
    

    ...then the two parents would have separate children arrays, but of course I'm mentioning this just for illustrative purposes, and you should declare array or object properties in the constructor as I showed above. In this example, the children array for parent1 is still referring to the children property on the prototype, so if you were to create a new Person object then it would still share children with Ebeneezer:

    var parent3 = new Person("Eve");
    console.log(parent3.children); //array containing Child A
    

    This article may also be helpful for understanding this: http://www.bennadel.com/blog/1566-using-super-constructors-is-critical-in-prototypal-inheritance-in-javascript.htm