Huckle
Huckle

Reputation: 1940

Method Inheritance in JavaScript

JavaScript uses a Prototype system, which is fundamentally different than a Class system. This is my first serious encounter with the language. I had fooled around with it previously, but this is the first time I built a system with proper OO, inheritance, polymorphism, etc.

From what I read there seems to be a few common methods to do member function inheritance in Javascript. Assuming you have a parent foo as following

foo = function(){ this.one = 1; }
foo.prototype.func1 = function(){return this.one;}

The MDN Introduction to JavaScript Inheritance suggests the naive approach of invoking the parent's method in the context of the child, as shown below.

bar = function(){ foo.call(this); }
bar.prototype = Object.create(foo.prototype);
bar.prototype.func1 = function(){ return this.one + foo.prototype.func1();}

This has the advantage of being simple to understand, but can become cumbersome as pointed out in this Salsify Blog post. The blog post outlines an alternate method where a super property is defined in the child prototype, and the name of each member function is attached as a property to the method. This method, however, relies on the caller property of a method, which the article points out will soon be deprecated. Rather than duplicate the entire post, I believe a summary of the important points are these

Object.defineProperty(bar.prototype, "super", {
 get: function get() {
  ...
  // methodName is set as a property on each method in an omitted code segment
  methodName = get.caller.methodName; 
  ...
  Object.getPrototypeOf(this.prototype)[methodName]
 }
}

Which is to say that you find the method with the same name in your prototype's prototype. I was wondering if this can be done in a simpler manner, without having to attach the method name as a parameter and without the Function.caller.

foo.prototype.super = function(method) {
  superMethod = Object.getPrototypeOf(this.constructor.prototype)[method];
  return superMethod.call(this, Array.prototype.slice.call(arguments, 1));
}

bar.prototype.func1 = function(){ return this.one + super('func1'); }

I'm making a number of assumptions in the above, I'd like to verify some assumptions.

  1. new bar().constructor.prototype === Object.getPrototypeOf(new bar())
  2. If the above is always true, is one preferable over the other?
  3. The Parent's member function will always live in the child's prototype's prototype (assuming that neither of the prototypes were mutated after object creation)
  4. That Object.getPrototypeOf() is not the "language support for accessing super methods" that the blog refers to as being added in ES6
  5. If Object.getPrototypeOf() isn't that language support, what is?

After seeing the error of using this, which does not change throughout the execution and always refers to the instance of the subclass, I've revisited and am thinking I need something like this

Grandfather = function(){};
Grandfather.prototype.function1 = function(){console.log("I am the Grandfather");};

Father = function(){Grandfather.apply(this);};
Father.prototype = Object.create(Grandfather.prototype);
Father.prototype.function1 = function f(){ f.super(); console.log("I am the Father");};
Father.prototype.function1.super = Grandfather.prototype.function1;

Child = function(){Father.apply(this);}
Child.prototype = Object.create(Father.prototype);
Child.prototype.function1 = function f(){ f.super(); console.log("I am the Child");};
Child.prototype.function1.super = Father.prototype.function1;

c = new Child();
c.function1();
// I am the Grandfather
// I am the Father
// I am the Child

And so the question becomes, how to set the super property on to each function in some automatic way?


One such way to do this is shown below, it has the benefit that functions added to the prototype after objects are instantiated still receive the benefit of being able to call superFunc, whereas an approach that sets a super property at class extension time would not set such a property if functions are added to the prototype later.

The downsides of this approach are that it only works in single threaded environment and that it requires functionality inherited from a common base class. It is not threadsafe since some state is held in a what is effectively a static variable of the function. This is fine for my purposes since browsers still have single threaded JavaScript. The requirement that all classes inherit from some base class containing this method isn't a huge blocker (especially if you do a "bad thing" and insert this into Object's prototype).

Grandfather.prototype.superFunc = function f(funcName){
  currentPrototype = Object.getPrototypeOf(f.startingPrototype || Object.getPrototypeOf(this));
  f.startingPrototype = currentPrototype;
  return currentPrototype[funcName]();
}

Child.prototype.function2 = function(){this.superFunc('function2'); console.log("Still in the Child");};
Father.prototype.function2 = function(){this.superFunc('function2'); console.log("Still in the Father");};
GrandFather.prototype.function2 = function(){console.log("Still in the Grandfather");};

c = new Child();
c.function2();
// Still in the Grandfather
// Still in the Father
// Still in the Child

Upvotes: 2

Views: 302

Answers (2)

HMR
HMR

Reputation: 39310

When you use super in the way you do it'll break when inheritance is more than 2 levels.

Assuming you'd use it the following way:

//changed super to this.super since super is not shown to exist in global scope
bar.prototype.func1(){ return this.one + this.super('func1'); }

See the following example:

function GrandFather(){
  this.i = 0;
};
GrandFather.prototype.test = function(){
  console.log('test in GrandFather');
};
function Father(){
  GrandFather.call(this);
};
Father.prototype = Object.create(GrandFather.prototype);
Father.prototype.constructor = Father;
Father.prototype.super = GrandFather.prototype;
Father.prototype.test = function(){
  console.log('test in Father');
  //prevent too many recursions
  this.i++;
  if(this.i>5){
    return;
  }
  this.super.test.call(this);//because test in child was called
   // with Child instance as invoking object this will be Child
   // and this.super will be Father.prototype
};
function Child(){
  Father.call(this);
}
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child;
Child.prototype.super = Father.prototype;
Child.prototype.test = function(){
  console.log('test in Child');
  this.super.test.call(this);//because invoking object is Child
    //this.super in Father is Child
};
var c = new Child();
c.test();

It's also common practice to start a constructor function with a capital so it's better to use Foo and Bar for constructor function names.

If you want to go through all the trouble of simulating super in JavaScript then the following way would be slightly more robust: http://ejohn.org/blog/simple-javascript-inheritance/

Upvotes: 1

Qantas 94 Heavy
Qantas 94 Heavy

Reputation: 16020

Question 1

new Bar().constructor.prototype should equal Object.getPrototypeOf(new Bar()), provided you haven't overrided Bar.prototype.constructor or Bar.prototype, or return a different object in the Bar constructor. Here's an example:

function Bar() {}
var foo = new Bar();
foo.constructor.prototype === Object.getPrototypeOf(foo); // true

function Bar2() {}
var foo2 = new Bar2();
Bar2.prototype = {};
foo2.constructor.prototype === Object.getPrototypeOf(foo2); // false

function Bar3() {}
var foo3 = new Bar3();
Bar3.prototype.constructor = function NotBar3() {};
foo3.constructor.prototype === Object.getPrototypeOf(foo3); // false

Question 2

If you're looking to get the actual prototype of an object, use Object.getPrototypeOf, as that's unaffected by any of the changes shown above.

Question 3

No, you will not be able to access Foo from new Bar(). In your example, new Bar() would not inherit from Foo.prototype and as a result, there's no way to access Foo unless you make it inherit from Foo.prototype or assign Foo to a property of new Bar() or Bar.prototype.

Question 4/5

No, that's not what they're referring to. ES6 will introduce a separate class contruct, where super takes on a special meaning (similar to how super works in other languages with classes). Here's an example of how classes work in ES6:

class Foo {
  constructor() {
    this.one = 1;
  }
  func1() {
    return this.one;
  }
}

class Bar extends Foo {
  func1() {
    return this.one + super();
  }
}

Upvotes: 3

Related Questions