My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, January 25, 2010

The JavaScript _super Bullshit

I know you already hate the title, but that's how I called one paragraph of my precedent post: Better JavaScript Classes.

This post is mainly dedicated for both libraries authors, and those Classic OOP Developers that still think JavaScript should be used in a classic way.

_super or parent Are Problematic!

It's not just about performances, where "the magic" may need to replace, wrap, and assign runtime everything in order to make it happens, it's about inconsistencies, or infinite loops, or hard debug, or disaster prone approach as well, since as I have already said instances have nothing to do with _super/parent .... so, how are things? Thanks for asking!

Real OOP Behavior

It's the ABC, and nothing else, if we call a parent/super inside a method, this method will execute with a temporary self/this context.
This means that the instance will be still:
  1. an instanceof its Class
  2. only that method will be executed, it is possible to call other super/parent accordingly with the hierarchy
  3. if the instance Class overrides a parent method, when this will be executed via super, the invoked "extra" method will still be the one defined in the instance class and nothing else

// Classic OOP Behavior, a simple PHP 5 example
class A {
public function __construct() {
echo 'A.__construct', '<br />';

// this will call the B method indeed
$this->hello();
}
public function hello() {
echo 'A.hello()', '<br />';
}
}

class B extends A {
public function __construct() {
// parent invocation, this has nothing to do with the instance
parent::__construct();
echo 'B.__construct', '<br />';
}
public function hello() {
echo 'B.hello()', '<br />';
}
}

new A;
// A.__construct
// A.hello()

new B;
// A.__construct
// B.hello()
// B.__construct

Even if most frameworks respect above behavior, there is still somebody convinced that _super/parent is something truly simple to implement, often replacing this references runtime without taking care of multiple hierarchies, more than a super, method specific super, and on and on ...
In few words, if above behavior is respected, and the best test case is with more than 2 extended classes, we could think we are half way there ... isn't it?

John Resig on Simple JavaScript Inheritance

John called it simple, and it is simple indeed. Inheritance in JavaScript has a name: prototype chain.
John did a good job for those few bytes, the runtime replaced method is temporarily the called one, so other methods will be invoked as expected.
Everything perfect? Not really, some "tiny little problem" could occur if for some reason something goes wrong.

var A = Class.extend({
init: function () {
document.write("A.constructor<br />");
},
hello: function () {
throw new Error;
document.write("A.hello()<br />");
}
});

var B = A.extend({
init: function () {
this._super();
document.write("B.constructor<br />");
},
hello: function () {
this._super();
document.write("B.hello()<br />");
}
});

setTimeout(function () {
alert(b._super === A.prototype.hello);
// TRUE!
}, 1000);
var b = new B;
b.hello();

This is what I mean when I talk about DPP, something goes wrong? The instance will be messed up.
Don't even think about a try catch for each method invocation, this will make your application extremely slow compared with everything else.
In any case, 5 stars for its simplicity, but think before you decide to use _super in any case.

MooTools Way

Update My apologies to MooTools team. I had no time to re-test a false positive and MooTools is partially secured behind its parent call. However, my point of view about chose magic is well described at the end of this post comments, enjoy.

The Prototype Way

So we have seen already _super and parent problems, both wrongly attached into the instance, but we have not seen yet the Prototype way: the $super argument!

var A = Class.create({
initialize: function () {
document.write("A.constructor<br />");
},
hello: function () {
throw new Error;
document.write("A.hello()<br />");
}
});

var B = Class.create(A, {
initialize: function ($super) {
$super();
document.write("B.constructor<br />");
},
hello: function ($super) {
$super();
document.write("B.hello()<br />");
}
});

setTimeout(function () {
alert(b);
// nothing to do
}, 1000);
var b = new B;
b.hello();

Eventually, Prototype library got it right, YES!!!
While every other is associating super to the instance, Prototype understood that super has nothing to do with the instance.
Every method which aim is to override the inherited one must have a $super argument, if we want a limitation but finally the only implementation that does not break anything, whatever happens in the super call.
This is correct, the instance is temporarily injected into the super method and nothing else. No self referencing, no run-time assignments that could break, simply the method, that will host for a call that instance.
Every other method will be wrapped in order to bring the current instance in the super one and in this way we can consider our code somehow safer, the whole application won't break if something goes unpredictably wrong!

Still Something To Argue About

While I have never thought that Prototype got it absolutely right, I must disagree about its Class implementation.
It's that simple, I have already said it, in JavaScript Classes are functions!
Accordingly, why on earth Class should be an object?

function Class(){
return Class.create.apply(null, arguments);
};
Object.extend(Class, {
// the current Class object
});
Class.prototype = Function.prototype;

With a ridiculous effort, Prototype library could be the first one to implement a proper Factory to create classes!

// valid, for OO "new" everything maniacs
var A = new Class(...);

// still valid, alias of Class.create
var B = Class(...);

// explicit Factory
var C = Class.create(...);

// there we are!
alert([
A instanceof Class, // true
B instanceof Class, // true
C instanceof Class, // true
A instanceof Function, // of course
B instanceof Function, // of course
C instanceof Function // of course
]);


How Things Work "There"

The second thing I must disagree about Prototype is the way arguments are retrieved.
The king of public prototypes pollution framework does not cache the Function.prototype.toString to avoid overrides and/or redefinition:

function Fake($super){};
alert(Fake.argumentNames());

Function.prototype.toString = function () {
return this.name || "anonymous";
};

// throws an error
// alert(Fake.argumentNames());

// Fake is a first class *Object*
Fake.toString = function () {
return "[class Fake]";
};
// throws an error
//alert(Fake.argumentNames());

Fake.toString = function () {
return "function Fake(){return Fake instances}";
};
// empty string
alert(Fake.argumentNames());

Are you kidding me? Let's try again:

Function.prototype.argumentNames = (function (toString) {
return function() {
var names = toString.call(this).match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
.replace(/\s+/g, '').split(',');
return names.length == 1 && !names[0] ? [] : names;
}
})(Function.prototype.toString);

4 out of 4 successful arguments parsing, rather than errors, problems, etc etc, for a framework that is already obtrusive, but at least in this case did not consider obtrusive code at all ... please fix it!

Inevitable Unportable

Finally, since the Prototype way requires wrappers and first argument injection, attached methods become instantly not portable anymore.
If we define a subclass and we would like to recycle its method somewhere else, we are trapped by the replaced, wrapped, injected $super argument.
Unfortunately this is the worst side effect for something that is actually not even close to classic OOP ... but we need to accept this compromise, or we simply need to better understand JavaScript, isn't it?

As Summary

This post does not want to be a "blame everybody for free" one, this post simply shows why I have written my Class implementation and why I do prefer explicit calls (somebody called them hard-coded) to super, shared, parent, whatever, inherited prototypes, as is for any other function we need in the middle of a session. JavaScript is like that, every function could be called without any warning somewhere with a different "this" reference and this is actually one of the most interesting and beauty part of this language.

I hope I have been able to give hints to both libraries authors, just 3 in this case, and specially to developers, those that copya and paste code trusting the source but unable to perform these kind of tests, and those convinced that JavaScript OOP and this._super is cool and it makes sense. As you can see, it does not, logically speaking first, and technically speaking after.
Last, but not least, all these wrappers simply mean less performances, even if we are talking about small numbers (a whole framework/libary based over these small numbers can easily become a pachyderm, seconds slower than many others).

21 comments:

Andrew Dupont said...

Glad to hear that Prototype "got it right," but as I recall we used the $super argument for other reasons.

The idea for our inheritance system came from Function#wrap and a desire to re-use that code. The proof-of-concept version I'd written used this.$super instead.

You're right, of course, that we should be using Function.prototype.toString, and I'm sure that's on kangax's list of things to fix. :-) The other stuff (Classes should be functions, methods can't be transferred to other objects by reference, etc.) seems to be a difference in taste.

SubtleGradient said...

Another excellent post, as usual.

Thanks for explaining to everyone how these implementations work. You have a good point of view.

MooTools used to have a much more elegant `this.parent` solution, but we had to change it because some browsers dropped support for it. I'll add some more specs for Class to make sure the issue you brought up is top of mind.

The way PrototypeJS injects $parent is pretty interesting.

I know you're entirely against the concept of `parent`. But I'd love to see if you could come up with a better way to do it than any existing implementation. (even though we know you'd never recommend that anyone use it)

I'll send you a free MooTools shirt if you find a better solution! :D We'll just need you to post an "Aylott" photo of yourself wearing it ofcourse ;)

Thanks!

SubtleGradient said...

How exactly did you test this?

Here is a jsbin of your example: http://jsbin.com/ijixu

I tested it in Firefox, Safari and IE6. In all three there is no infinite loop.

Could you post a jsbin of exactly what html & js you tested? What browser did you use? What version of MooTools? etc…

khs4473 said...

Agree with you completely. Awhile back I was into the whole "subclassing" thing and built that into my library. I took a look at the options for calling methods in the parent class (including using a super argument) and nothing made sense. I just ended up calling the original function directly, by name (hard-coding, as you put it). Everything else is just smoke-and-mirrors.

I have since given up on subclassing in javascript. I find that I simply don't need it to express the concepts that I want to express. It's sometimes useful to be able to add to a prototype (for doing plug-in kind of work) but I typically wrap the "new" call behind a simple function interface. I use the new operator as an implementation detail, rather than part of the library API.

Thanks for sharing!!

kentaromiura said...

-- Accordingly, why on earth Class should be an object?

Andrea, in the smalltalk way of thinking, accordly to wikipedia

Since classes are themself object, because in a REAL OO Language classes are objects, they can be asked questions such as "what methods do you implement?" or "what fields/slots/instance variables do you define?". So objects can easily be inspected, copied, (de)serialized and so on with generic code that applies to any object in the system.


BTW we all know that javascript are a totally different beast, so take this only as a sidenote

Andrea Giammarchi said...

@kentaromiura functions ARE objects indeed, first class objects.
My point is that in that way Class will become like Function, Object, RegExp, and Array constructors - in few words a proper factory, where since we are trying to emulate classic OOP, new Class will produce an instanceof Class ;)

Others, thanks for your posts, hope it will be another interesting conversation.

MRoderick said...

Am reading these posts with great interest, and am often confused about WHY a lot of developers feel the need to force the Classical inheritance pattern into JavaScript.

To quote Crockford:
The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.

... and to quote one of my colleagues:
I think jamming class-based inheritance into a language that doesn't really support it makes for *more* confusing code... especially if you're a programmer who does actually understand prototype-based stuff ... ie, you're peanalizing your best developers.

So can we just get back to learning JavaScript, instead of trying to make it Java?

wpbasti said...

Just wonder what you think about using arguments.callee for super calls. In qooxdoo we attach the super method to every method we attach to a class when it exists.

B.hello.base => A.hello

So we can use:
arguments.callee.base.call(this, arg1, arg2, ...);

to call the super method.

This works pretty well. In mixins we do not support this base call as mixins might be mixed into different classes.

Andrea Giammarchi said...

wpbasti unfortunately arguments and specially callee, are not a solution to me.

Please have a look

wpbasti said...

@Andrea: Any why do you think so? Can you give some details. Would be pretty comfortable way in implementing it.

khs4473 said...

arguments.callee throws an error in ES5 "strict" mode, correct?

Andrea Giammarchi said...

correct, but just arguments is too heavy to discover ... avoid it when possible! The link I have posted my last comment has a bench inside ;)

Steven Roussey said...

Ext.menu.TextItem = Ext.extend(Ext.menu.BaseItem, {
constructor : function(config){
...
Ext.menu.TextItem.superclass.constructor.call(this, config);
}
...
});

This puts the superclass where it belongs...

Andrea Giammarchi said...

well, true that it belongs there, but:


Ext.menu.TextItem.superclass.constructor.call(this, config);

VS

Ext.menu.BaseItem.call(this, config);

OR

Ext.menu.BaseItem.prototype.constructor.call(this, config);

both shorter, explicit, less redundant?

khs4473 said...

I've been thinking about the super problem and I've decided to put together a little something of my own. It avoids wrapping but provides a convenient way to call methods from the super class. If performance is critical then super methods can be called directly. I'd like to know what you (or anyone else) thinks about it.

The code can be found here.

Thomas Aylott said...

I'm glad that you silently updated your post to remove your error about the MooTools code.
It would have been better to actualy admit that you were wrong however.

But I still object to your claim that "One single error and the entire application … could be messed up".

Your example code is complete nonsense. The code has both `throw new Error` and `document.write` in a timeout. In what universe would that not throw an error?

I'm still waiting for some proof that MooTools' implementation of `this.parent` is in any way buggy.

Prove it or admit that you are wrong.

Andrea Giammarchi said...

Thomas, the fct there is a strike, rather than a silent update as you mention, means I have already said what I wrote was wrong (showing there was something wrong before).

Your example code is complete nonsense
if you don't get it, it does not mean it's wrong.
document.write has nothing to do with the example while a timeout is something that happens on daily basis via both Ajax and animations/transictions.

The error is manually throwed emulating an unexpected or uncaught problem and after that the instance method is broken.

Unless MooTools will implement a try catch for every method execution, this is always true, and if you think this is good for a developer, well, you have to convince me and thousands more about what you think a good program is.

Regards

Andrea Giammarchi said...

Thomas, if I am not wrong you are one MooTools core developer, right?

Well, I hope your team is not like you. Libraries author should accept evidences/probles/suggestions in a bit more flexible way, otherwise libraries improvements are compromised.

If authors feel like "we are cool, everybody else sucks", since you have even been unable to understand the example, I don't think these devs can bring any benefit.

Finally, just in case you are wondering, document.write was there to test the behavior in browsershots.org, so next time you find it in an example, think before blaming everywhere, OK?

Regards

Thomas Aylott said...

Do you really have to personally attack me?


I apologize if I've implied anything like that. But, I said nothing of the kind. Stop putting words in my mouth.

Quite the oposite, it's very clear that you have no respect for MooTools or our developers. Your bias blinds you and colors your statements.

I'm simply trying to find some nugget of truth in this confusing and innaccurate post.

If there is really something wrong with our code I'd love to fix it.

So far, you have shown us nothing that proves there is an issue with MooTools' code.

I proved that the error you were claiming was not real.

If there really is an error, please explain what it is and how to reproduce it.

Your example proves that calling a method that calls a method that throws an error throws an error. That is the expected result. The instance method does not get "broken".

Calling a method that throws an error will throw an error, no matter if you're using MooTools' `this.parent()` or `ClassA.prototype.methodName.call(this)`. It's going to have the same exact result!

Are you trying to claim that there is a different problem?

If so, please explain the real issue in greater detail.

Andrea Giammarchi said...

Thomas you are right, I had a false positive in Firebug and I will correct the MooTools problem ASAP.

In any case, the point is is that a single problem caused just once could mess up an instance forever.

Just write this._current = {}, or whatever value different from null and undefined, before a this.parent call, and the instance will be useless, the throwed error will be always in the same wrapped code inside MooTools library, and both debug and consistency are done.

What I have said in the whole article is that parent has nothing to do with the instance, but obviously in JavaScript becomes natural to attach runtime everything to a single instance, right?

And this is to emulate what, classic OOP pattern?

In classic OOP I use parent::whateverMethod(), I don't refer to parent as the current method and nothing else, you know what I mean?

This is what I consider "bad magic".

As example, MooTools, which is a great framework in any case, decided that "_current" is a reserved word while it could be without any problem an iterator protected variable.

Moreover, being call and apply always there and protected properties an utopia, specially for "for in" loops, and being JavaScript usage rarely sandboxed in a single framework, do you agree that at least MooTools should use bloody unobtrusive names to make the magic happen?

Or the suggestion here is that everybody should learn internals in order to do not break their code by them self?

And to obtain what, a faked classical misconception about parent?

_super/parent is bullshit in JavaScript, and the language itself as is, cannot change my point of view.

Apologies if I have offended MooTools guys, but I am sorry, you are not providing a proper parent meaning, and you could have convention because of wrong properties name choice.

Best Regards

Andrea Giammarchi said...

and you could have convention because of wrong properties name choice.

I meant:
and you should have convention because of wrong obtrusive properties name choice.