Search code examples
javascriptinheritanceclone

Eloquent Javascript LifeLikeTerrarium clone


I am going through Marijn Haverbeke's Eloquent Javascript and am having some trouble understanding a section of the LifeLikeTerrarium example. Specifically the code below:

function clone(object){
    function OneShotConstructor(){}
        OneShotConstructor.prototype = object;
        return new OneShotConstructor();                
}

Which is called by the constructor for LifeLikeTerrarium:

function LifeLikeTerrarium(plan){
    Terrarium.call(this, plan);
}           
LifeLikeTerrarium.prototype = clone(Terrarium.prototype);
LifeLikeTerrarium.prototype.constructor = LifeLikeTerrarium;

My understandig is that LifeLikeTerrarium should inherit from Terrarium and this code is trying to make that happen. My uncertainty is about how it achieves this.

  • Does doing the 'Terrarium.call(this, plan) pass the data to the Terrarium constructor as if you had called Terrarium(plan) within the context of the inheritence?
  • What is the purpose of calling clone(object)? How is this different from saying "LifeLikeTerrarium.prototype = Terrarium.prototype?
  • What is the purpose of assigning LifeLikeTerrarium's constructor to itself?

I feel like I understand the rest of the code but this seems like a pretty crucial section. Would really appreciate anyone who is willing to break it down for me.

Thanks!


Solution

  • Does doing the Terrarium.call(this, plan) pass the data to the Terrarium constructor as if you had called Terrarium(plan) within the context of the inheritence?

    Yes. Function#call calls the function with a given this value and arguments provided individually. Meaning we specify the this inside the called function to be whatever object we want. Terrarium.call(this, plan) passes our current this (which will be an instance of LifeLikeTerrarium) to the call to Terrarium. If Terrarium assigns something or modifies this inside itslef, the this it modifies will be what we passed to it using call, here the current instance of LifeLikeTerrarium. The rest of the parameters to call are all passed along to the called function as individual paramaters.

    Demonstration:

    function foo(a, b) {
        this.sum = a + b;
    }
    
    var obj = {};
    
    foo.call(obj, 5, 7);                // obj will be the 'this' used inside foo.
                                        // 5 and 7 will be the values of foo's arguments a and b, respectively
    
    console.log(obj);                   // et voilà

    What is the purpose of calling clone(object)? How is this different from saying LifeLikeTerrarium.prototype = Terrarium.prototype?

    We don't want to do LifeLikeTerrarium.prototype = Terrarium.prototype. Ever. Because LifeLikeTerrarium is most likely going to have methods of its own that we don't want added to Terrarium as well. Imagine we add a method to LifeLikeTerrarium's prototype called foo. foo will also be available to Terrarium. Even worse, foo could replace Terrarium's own method foo, if it happens to have one:

    function Terrarium() {  }
    Terrarium.prototype.sayHi = function() {
        console.log("Hi! I'm Terrarium");
    }
    
    function LifeLikeTerrarium() {  }
    LifeLikeTerrarium.prototype = Terrarium.prototype;    // now LifeLikeTerrarium.prototype and Terrarium.prototype are both referencing the same object, changes to one are reflected on the other
    LifeLikeTerrarium.prototype.sayHi = function() {      // Terrarium.prototype.sayHi is gone for ever. May it rest in peace.
        console.log("Hi! I'm LifeLikeTerrarium");
    }
    
    var terrarium = new Terrarium();
    terrarium.sayHi();                                    // wrong wrong wrong

    As you can see, this is very bad. Depending on what the method does, it could even throw an error and break the execution (if for example the new method uses properties that aren't in the original object Terrarium):

    function Terrarium() {  }
    Terrarium.prototype.sayHi = function() {
        console.log("Hi! I'm an abstract Terrarium that doesn't have a name");
    }
    
    function LifeLikeTerrarium(name) {
        this.name = name;
    }
    LifeLikeTerrarium.prototype = Terrarium.prototype;
    LifeLikeTerrarium.prototype.sayHi = function() {
        console.log("Hi! I'm " + this.name.toUpperCase());  // b.. b.. but Terrarium doen't have a property called name. Let's see what will happen. Maybe it'll work 
    }
    
    var terrarium = new Terrarium();
    terrarium.sayHi();                                      // Surprise! You got mail, I mean an error

    In addition to this, we, at the very least, will be poluting the original object with methods that it won't even need or use:

    function Terrarium() {  }
    
    function LifeLikeTerrarium() {  }
    LifeLikeTerrarium.prototype = Terrarium.prototype;
    LifeLikeTerrarium.prototype.sayHi = function() {      // now Terrarium has a method called sayHi although it originally didn't have one.
        console.log("Hi!");
    }
    
    var terrarium = new Terrarium();
    terrarium.sayHi();                                    // Want proof? Here you go.

    How do we get arround this? The answer is: We don't assign Terrarium.prototype to LifeLikeTerrarium.prototype so that both prototypes reference the same object. No, we assign to LifeLikeTerrarium.prototype a plain new object that has its prototype set to Terrarium.prototype. The new plain object (which is the result of new OneShotConstructor()) will act as LifeLikeTerrarium's own prototype. That plain object will have its prototype set to Terrarium.prototype which causes Terrarium's methods to be available to LifeLikeTerrarium, a phenomenon that goes by the name of "Inheritance". Here is a diagram of the difference between the two ways:

    LifeLikeTerrarium.prototype = Terrarium.prototype:

    Terrarium.prototype ---------------------> {
                                   |                ...
                                   |                ...
                                   |           }
                                   |
    LifeLikeTerrarium.prototype ---/
    

    LifeLikeTerrarium.prototype = clone(Terrarium.prototype):

    Terrarium.prototype ---------------------> {
                                   |                ...
                                   |                ...
                                   |           }
                                   |
                                   \------------------------------\
                                                                  |
    LifeLikeTerrarium.prototype -------------> {                  |
                                                    ...           |
                                                    ...           |
                                                    prototype: ---/
                                               }
    

    As you can see, in the first diagram, both Terrarium.prototype and LifeLikeTerrarium.prototype are referencing the same object. Whereas in the second diagram each is referencing it's own object: Terrarium.prototype is referencing its own object which is defined somewhere in your code and LifeLikeTerrarium.prototype is referencing an instance of OneShotConstructor (which is an empty object because the constructor is an empty one). Another benifit is that LifeLikeTerrarium.prototype's prototype is referencing Terrarium.prototype (made possible because of OneShotConstructor.prototype = object;). This creates a nice prototype chain that lets methods of Terrarium be available to LifeLikeTerrarium if the latter doesn't want to have its own, and if it does (want to have its own), that will be totally fine because they won't replace Terrarium's, they'll just shadow them. And this is the essence of prototype inheritence.

    What is the purpose of assigning LifeLikeTerrarium's constructor to itself?

    This already has an answer here: Why is it necessary to set the prototype constructor?