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.
This means that the instance will be still:
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 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.
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.
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!
It's that simple, I have already said it, in JavaScript Classes are functions!
Accordingly, why on earth Class should be an object?
With a ridiculous effort, Prototype library could be the first one to implement a proper Factory to create classes!
The king of public prototypes pollution framework does not cache the Function.prototype.toString to avoid overrides and/or redefinition:
Are you kidding me? Let's try again:
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!
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?
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).
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:
- an instanceof its Class
- only that method will be executed, it is possible to call other super/parent accordingly with the hierarchy
- 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).
Comments
Post a Comment