Luke
Luke

Reputation: 5708

JS Inheritance: calling the parent's function from within the child's function

There must be something I don't understand about the JS object model.

From these resources:

I have gathered what I think, or thought, was an accurate mental representation of the object model. Here it is:


All objects have a property, which the docs refer to as [[Prototype]]. [[Prototype]] can be thought of as a reference to the object's parent. More accurately:

The reference to the [parent's] prototype object is copied to the internal [[Prototype]] property of the new instance. (source 1)

You can get access to the [[Prototype]] property of the child with Object.getPrototypeOf(child) The value returned here will be a reference to the parent's prototype (not its internal [[Prototype]] property, but its actual prototype)

obj.prototype is different from the object's internal [[Prototype]] property. It acts like the blueprints used to make instances of this exact object, while its [[Prototype]] property points to the blueprints used to make instances of its parent.

To elaborate:

You can use Object.create() to create a new object with whatever [[Protoype]] property you want. So you can use it as a way to implement inheritance. See source 2 for an example.


With this in mind, I wanted to get a working example of my own going. My plan was to create a parent 'class' and then make a child 'class' that inherited from it.

I wanted the child class to implement a method, which overloaded a method from the parent. The caveat is that I want the child's version of the method to call the parent's version of the method and then do some extra stuff.

Here is what I came up with, see below for the issues associated with it:

var Parent = function() {};

Parent.prototype.myF = function() {
  console.log('derp');
};


function Child() {
  Parent.call(this);
};

//make [[prototype]] of Child be a ref to Parent.prototype
Child.prototype = Object.create(Parent.prototype);

//need to explicitly set the constructor
Child.prototype.constructor = Child;

Child.prototype.myF = function() {
  Object.getPrototypeOf(this).myF();
  console.log("did I derp??");
};

childInstance = new Child();
childInstance.myF();

It appears to be the case that when I attempt to overload Parent.myF(), while I am overloading it, I am actually modifying the original function at the same time. This appears to be the case because the logged results are:

'derp'
'did I derp??'
'did I derp??'

presumably the first occurance of 'did I derp??' is coming from a modified version of the parent's function, which I don't mean to do, then the second version is coming from the child's function.

Can anyone elaborate on why this is happening?

Upvotes: 13

Views: 8567

Answers (3)

tech-e
tech-e

Reputation: 462

If you have to work with inheritance in ES5 I have made a wrapper that makes it easy to extend and call parent methods. No frameworks required.

fiddle to working code here: https://jsfiddle.net/wcrwLmrk/13/

Here is how the code works:

/* extend the base class to the new parent class and pass 
an object with the new properties and methods */
var ParentClass = Class.extend(  
{ 
    // reset the constructor to the correct constructor
    constructor: function() 
    { 

    },

    callName: function(arg)
    { 
        console.log('parent ' + arg); 
    }
}); 

/* extend the parent class to the new child class and pass 
an object with the new properties and methods */
var ChildClass = ParentClass.extend(  
{ 
    // reset the constructor to the correct constructor
    constructor: function() 
    { 
        ParentClass.call(this); 
    },

    callName: function(arg)
    { 
        // child method code
        console.log('child ' + arg);

        /* call parent method by passing the method name and any params */ 
        this.super('callName', arg); 
    }
}); 

Now we can create a new child class that will call the super class method:

var child = new ChildClass(); 
child.callName('name');

Here is the base class that needs to be included in the project:

var Class = function() 
{ 

}; 

Class.prototype =  
{ 
    constructor: Class, 

    /* this is an alias for callParent. this will 
    allow child classes to call super methods. 
    @param (string) method name 
    @param [(mixed)] addasmany args as the super 
    method needs 
    @return (mixed) the super method return value */
    super: function()
    { 
        var parent = this.parent; 
        if(parent)
        { 
            var args = [].slice.call(arguments), 

            // this will remove the first arg as the method
            method = args.shift(); 
            if(method)
            { 
                var func = parent[method]; 
                if(typeof func === 'function')
                { 
                    return func.apply(this, args);
                } 
            } 
        }
        return false;
    }
}; 

/* this will return a new object and extend it if an object it supplied. 
@param [(object)] object = the extending object 
@return (object) this will return a new object with the 
inherited object */ 
var createObject = function(object) 
{ 
    if(!Object.create) 
    { 
        var obj = function(){}; 
        obj.prototype = object;
        return new obj(); 
    } 
    else 
    { 
        return Object.create(object); 
    } 
}; 

/* this will extend an object and return the extedned 
object or false.  
@param (object) sourceObj = the original object 
@param (object) targetObj = the target object */ 
var extendObject = function(sourceObj, targetObj) 
{ 
    if(typeof sourceObj !== 'undefined' && typeof targetObj !== 'undefined') 
    { 
        for(var property in sourceObj) 
        { 
            if(sourceObj.hasOwnProperty(property) && typeof targetObj[property] === 'undefined') 
            { 
                targetObj[property] = sourceObj[property]; 
            } 
        } 

        return targetObj; 
    } 
    return false; 
}; 

var extendClass = function(sourceClass, targetClass) 
{ 
    /* if we are using a class constructor function 
    we want to get the class prototype object */  
    var source = (typeof sourceClass === 'function')? sourceClass.prototype : sourceClass, 
    target = (typeof targetClass === 'function')? targetClass.prototype : targetClass;

    if(typeof source === 'object' && typeof target === 'object') 
    { 
        /* we want to create a new object and add the source 
        prototype to the new object */ 
        var obj = createObject(source); 

        /* we need to add any settings from the source that 
        are not on the prototype */ 
        extendObject(source, obj); 

        /* we want to add any additional properties from the 
        target class to the new object */ 
        for(var prop in target) 
        { 
            obj[prop] = target[prop]; 
        } 

        return obj; 
    } 
    return false; 
}; 

/* this will allow the classes to be extened. 
@param (object) child = the child object to extend 
@return (mixed) the new child prototype or false */ 
Class.extend = function(child)
{  
    if(!child)
    { 
        return false; 
    } 

    var parent = this.prototype; 

    /* the child constructor must be set to set 
    the parent static methods on the child */ 
    var constructor = child && child.constructor? child.constructor : false; 
    if(child.hasOwnProperty('constructor') === false)
    { 
        constructor = function()
        {
            var args = arguments; 
            parent.constructor.apply(this, args); 
        }; 
    }

    constructor.prototype = extendClass(parent, child); 
    constructor.prototype.parent = parent;

    /* this will add the static methods from the parent to 
    the child constructor. */ 
    extendObject(this, constructor); 
    return constructor; 
};

Upvotes: 0

nils
nils

Reputation: 27174

Great question, it took a bit of testing and researching to find it out.

Identifying the strange behaviour

I changed your code a little bit to find out which function is called when:

var Parent = function() {};

Parent.prototype.myF = function() {
  console.log('derp');
};


function Child() {
  Parent.call(this);
  this.name = 'Test'; // this is a new test property
};

//make [[prototype]] of Child be a ref to Parent.prototype
Child.prototype = Object.create(Parent.prototype);

//need to explicitly set the constructor
Child.prototype.constructor = Child;

Child.prototype.myF = function() {
  console.log(this); // here I want to find out the context, because you use it in the next line
  Object.getPrototypeOf(this).myF();
  console.log("did I derp??");
};

childInstance = new Child();
childInstance.myF();

You can check out the JSFiddle and try it for yourself: http://jsfiddle.net/Lpxq78bs/

The crucial line in your code is this:

Child.prototype.myF = function() {
  Object.getPrototypeOf(this).myF(); // this one
  console.log("did I derp??");
};

After doing a console.log(this); to find out what this refers to, I saw that it changes between the first and the second output of did I derp??.

I got the following output:

Object { name: "Test" }
Object { constructor: Child(), myF: window.onload/Child.prototype.myF() }
"derp"
"did I derp??"
"did I derp??"

Interpreting the Output

Since I added a 'name' property to the Child constructor, it would only be around if I am looking at an instance of Child, not at its .prototype.

So the first line of the Output means that the current this context is indeed the childInstance. But the second one is neither the childInstance, nor the Parent.prototype:

  1. Call (myF of childInstance): this refers to the childInstance. Object.getPrototypeOf(this).myF(); then looks for the [[Prototype]] of the childInstance, which is the Child.prototype, not the Parent.prototype. Output: 'did I derp??'

  2. Call (myF of Child.prototype): this refers to the Child.prototype, which is the childInstances [[Prototype]] Property. So the second call of Object.getPrototypeOf(this).myF(); finally returns the Parent.prototype (sort of). Output: 'did I derp??'

  3. Call (myF of Parent.prototype instance created by Object.create): Finally, the myF on the parent is called. Output: 'derp'

Since your console.log("did I derp??") comes after the myF function call, the output is in reverse order. The following graphic illustrates how the code is traversed:

enter image description here

So your assumption about what Object.getPrototypeOf(this).myF(); refers to, was wrong.

Solution in ES5

By @LukeP: https://jsfiddle.net/Lpxq78bs/28/

Alternative Solution in ES6

To avoid this confusion, and since you are working with a classical inheritance pattern, you could have a look at ES6 Classes. The following would be a rough example of what you are trying to do:

class Parent {
    constructor() {
      
    }
    
    myF() {
        console.log('derp');
    }
}

class Child extends Parent {
    constructor() {
        super();
    }

    myF() {
        super.myF();
        console.log("did I derp??");
    }
}

var childInstance = new Child();
childInstance.myF();

I hope this helps in understanding what happens.

Upvotes: 6

bugwheels94
bugwheels94

Reputation: 31920

Your code is working as expected , the output you are getting is because of Object.getPrototypeOf and can be explained by

Step 1 : When you cal childInstance.myF(); then it goes to below code where this refers to childInstance itself

Child.prototype.myF = function() {
  Object.getPrototypeOf(this).myF();
  console.log("did I derp??");
};

then Object.getPrototypeOf returns childInstance.[[Prototype]] which is Child.prototype and again call myF method(leaving second line to printed later after the execution of the method) but next time this will reference to childInstance.[[Prototype]].

Step 2 : In this call this points to childInstance.[[Prototype]]( or Child.prototype object itself)

Child.prototype.myF = function() {
  Object.getPrototypeOf(this).myF();
  console.log("did I derp??");
};

Now Object.getPrototypeOf returns childInstance.[[Prototype]].[[Prototype]]( which is Child.prototype.[[Prototype]]) which is Parent.prototype and again call myF method but this time myF method will print derp because myF method of Parent.prototype is being called. After that, second line will print did i derp.

Step 3 : Then the function comes back to step 1 to execute second line which again prints did i derp

Upvotes: 6

Related Questions