1

I was reading this article about super methods in Javascript. Right at the bottom, there's an approach used by the author, which essentially involves adding a name property to every method function object and using it to find a matching method on the prototype chain of the current object calling super.

I'll replicate the code below:

var Base = function() {};

// Use the regular Backbone extend, but tag methods with their name
Base.extend = function() {
  var Subclass = Backbone.Model.extend.apply(this, arguments);
  _.each(Subclass.prototype, function(value, name) {
    if (_.isFunction(value)) {
      value._methodName = name;
    }
  });
  return Subclass;
};

// Define a special `super` property that locates the super implementation of the caller
Object.defineProperty(Base.prototype, "super", {
  get: function get() {
    var impl = get.caller,
      name = impl._methodName,
      foundImpl = this[name] === impl,
      proto = this;

    while (proto = Object.getPrototypeOf(proto)) {
      if (!proto[name]) {
        break;
      } else if (proto[name] === impl) {
        foundImpl = true;
      } else if (foundImpl) {
        return proto[name];
      }
    }

    if (!foundImpl) throw "`super` may not be called outside a method implementation";
  }
});

It uses Underscore.js and Backbone.js, which I'm not very familiar with, but their usage is not where the doubt lies.

When setting the super property getter, 4 variables are declared: impl, name, foundImpl and proto.

impl holds the method that called super, obtained through get.caller. name is the name of the method that called super (or undefined if super is not called from within a method). proto holds the object from where the method was invoked, whose prototype chain will be transversed until we find the super method.

Now foundImpl is a bit confusing for me. It is a boolean, and at first is assigned the value of this[name] === impl. Since this points to the object from where the method is being invoked from, this[name] will return the method itself, which will be === to impl. This will be true everytime, unless name is undefined (we call super outside a method).

The line while(proto = Object.getPrototypeOf(proto)) then starts iterating through the prototype chain of the calling object, starting from the immediate parent, until it reaches null.

if(!proto[name]) checks if there is a method with the same name in the current prototype. If there isn't it breaks out of the loop and if foundImpl is false an error will be thrown. As stated before, the only situation where I can see this happening is if super is called outside a method, where name will be undefined and thus this[name] === impl will be false as well. Otherwise, since foundImpl will already be true from the beginning.

else if (proto[name] === impl) will check if the current prototype method with the same name is strictly equal to the method that is calling super. I honestly can't think of a situation where this will be true, since for super to be called from a method it will have to be overriden, making both different function objects. For example:

var a = { method: function(){ return "Hello!"; } };
var b = Object.create(a);
console.log(a.method === b.method); //true
b.method = function(){ return "Hello World!"; };
console.log(a.method === b.method); //false

Maybe this is just a safety check after all and this conditional will never be reached?

Finally, else if (foundImpl) will check is foundImpl is true (which it will likely be on the first loop iteration, except on the special case stated above) and return the current proto[name] method if it is.


So my doubt is what is the point of that second conditional: else if (proto[name] === impl)? What case is it covering? What is the role of foundImpl after all?

1 Answer 1

1

Wow, I haven't thought about that blog post in quite a while! I believe most evergreen browsers have dropped support for arguments.caller at this point, which makes demo code a little hard to write, but I'll do my best to explain 🙂

The line you called out as being confusing is foundImpl = this[name] === impl, which looks at first glance like it would always evaluate to true. The reason it's needed (and actually what interested me about "the super problem" in the first place) is cases where you have chained super calls.

Consider a setup like this:

const Base = /* ... */;

const First = Base.extend({
  sayHello() {
    console.log('First#sayHello()');
  }
});

const Middle = First.extend({
  sayHello() {
    this.super();
    console.log('Middle#sayHello()');
  }
});

const Last = Middle.extend({
  sayHello() {
    this.super();
    console.log('Last#sayHello()');
  }
});

If I call new Last().sayHello(), that super getter will be invoked twice. The first time, as you say this[name] === impl will be true right off the bat.

In the second call, though, the caller will be the function from Middle, but this[name] will be the function from Last, so this[name] === impl will be false.

Execution will then continue into the prototype-traversal loop, and after going up one step, we'll find that proto[name] === impl, so foundImpl will be set to true, and we'll proceed through the loop one more time and correctly return the function from Base.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot Dan, perfect explanation! It all makes sense now :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.