Skip to content

Instantly share code, notes, and snippets.

@Zirak
Last active September 7, 2016 16:23
Show Gist options
  • Select an option

  • Save Zirak/cd319399c89e230c60a4 to your computer and use it in GitHub Desktop.

Select an option

Save Zirak/cd319399c89e230c60a4 to your computer and use it in GitHub Desktop.
Method overriding in js prototypical inheritance with proxies

So

you have an object.

var o = {
    a : 4,

    log : function () {
        console.log('a:', this.a);
    }
};

You make another one.

var p = Object.create(o);
p.a = 7;
p.log = function () {
    console.log('zippededee');
    // how duz I call o.log on this object?
};

We overrode o.log, so we can’t just do this.log, it’s infinite recursion. The simplest solution would be o.log.call(this), but that’s ugly: We have to know who our parent is, and there’s that call.

How about this, then: Object.getPrototypeOf(this).log.call(this)

…I don’t think I have to explain why this isn’t a good and feasible solution in the long run.

Whatever can we do!?

Enter proxies

“Don’t worry”, say proxies, “I shall save your ass!”

“Shave my ass?” you ask, bewildered “…how did you know my ass is hairy?”

“I..uh…what?” proxies reply, equally bewildered, “I said save your ass, not shave your ass”.

“Oh”, you say, “that’s allright then.”

“…weirdo”.

Proxies! We can define a property (say super) on the child object which proxies all property accesses to the prototye object, and when we request a function, bind it to the child! In our example above, if p.super was such a proxy:

p.log = function () {
    console.log(this.a); // 7

    console.log(this.super.a); // 4, since o.a === 4
    this.super.log(); // a: 7
};

Neat, huh? this.super functions as a shorthand for Object.getPrototypeOf(this), with the awesome side effect that functions are automatically bound to this.

The implementation isn’t tricky at all, but there is a very big drawback. Give the code a read over, think it over, and come back here. I’ll wait.

Drawback

Did you finish? Did you really finish, or are you just saying it? Okay, I believe you. Can you think where this method falls short? Let’s do the example over using Object.inherit.

var o = {
    a : 4,

    log : function () {
        console.log('a:', this.a);
    }
};

var p = Object.inherit(Object.create(o), {
    a : 7,

    log : function () {
        console.log('zippededee');
        this.super.log();
    }
});

p.log(); // works as planned

// But what if we add another object?

var q = Object.inherit(Object.create(p), {
    a : 14,

    log : function () {
        console.log('mooo');
        this.super.log();
    }
});

q.log(); // What happens now?

Think it over a bit. It took longer than I’m willing to admit to debug it.

We get a RangeError because of infinite recursion. q.super.log() binds p.log to q, so when inside p.log we call this.super.log, what’s actually called is q.super.log, which is again p.log, and so forth until the tomatoes take over.

I have no solution, because that’s correct behaviour according to the system because of the dynamity of the this keyword (which we use). So know this exists. Also, you probably don’t want to use any of this anyway, so win.

Object.inherit = function (base, child) {
return Object.superfy(Object.mergeToLeft(base, child));
};
Object.superfy = function (obj) {
var parent = Object.getPrototypeOf(obj);
var proxy = {
// The get's job is to go against the parent object and grab the desired
//value. If it's a function, we bind it against our object.
// To save on functions, there's a cache of keys we've already bound.
//Whether it helps or not isn't actually tested, but meh. There are worse
//things to not test. Like whether your toaster burns your crotch.
get : function (receiver, key) {
var val = parent[key];
// We only care about functions.
if (!val || !val.call || !val.bind) {
return val;
}
// Grab the bound value from cache, or make one if it doesn't exist.
var cached = obj._super_cache[key],
bound;
if (!cached || cached.unbound !== val) {
bound = val.bind(obj);
// We keep the unbound value around so we can have something to
//to test against.
obj._super_cache[key] = {
unbound : val,
bound : bound
};
}
else {
bound = cached.bound;
}
return bound;
},
set : function () {
// TODO reconsider.
return false;
}
};
Object.defineProperty(obj, 'super', {
value : Proxy.create(proxy, obj),
// TODO reconsider.
configurable : true
});
Object.defineProperty(obj, '_super_cache', {
value : Object.create(null),
configurable : true,
writable : true
});
return obj;
};
Object.mergeToLeft = function (left, right) {
return Object.keys(right).reduce(addKey, left);
function addKey (ret, key) {
ret[key] = right[key];
return ret;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment