PDA

View Full Version : annoying design flaw in ExtJS



mindplay
11 Dec 2008, 11:01 AM
Please consider the following for future versions of ExtJS or other frameworks.

In many places throughout Ext, callbacks are invoked in "clever" ways - using Function.call() to override the calling context (this)...

This is a tempting feature in JavaScript, as it often enables you to enforce a calling context that you may think makes more sense than whatever calling context a function would have naturally been called in.

But more than often, this causes all manner of confusion, and in some cases makes it nearly impossible to get to the "real" context of whatever method you're having some component invoke as a callback.

A good example is renderers in the GridPanel.

Renderers are forcibly invoked using the <td> element as the calling context. While this may seem practical, or even natural, it can really get in the way of what you're trying to do.

Let's say I want to use a method of some other object, say of Class X, as a renderer.

And let's say that the calling context is for some reason important to me - that my rendering function needs to know which instance of X it belongs to.

The natural way to write this, would be:



Class_X = Ext.extend(Class_Y, {
constructor: function(options) {
... (assign this.renderer to column config) ...
},
renderer: function(...) {
return '...' + this.getSomething() + '...';
}
});


This will not work, as "this" in the renderer() method will be overridden at call-time.

Because the real calling context, my instance of X, is being overridden, I have effectively no way to determine which object this renderer belongs to.

The only way to work around this, is to manually create my function, as a local variable, inside my constructor, for example:



Class_X = Ext.extend(Class_Y, {
constructor: function(options) {
var me = this;
var renderer = function(...) {
return '...' + me.getSomething() + '...';
};
... (assign renderer to column config) ...
}
});


This is not a pretty workaround, and should not be necessary.

The point I'm trying to make here, is that you could just as well pass that <td> reference as an argument, instead of overriding the real calling context.

The calling context is there for a reason, and is key to a lot of aspects of good object-oriented design. Being able to identify the object to which a method belongs, is crucial.

Context override was not meant as a replacement for regular function arguments, which is how I feel it's sometimes being used in Ext.

Overriding the calling context often means that the user has to guess what the calling context might be. For example, the documentation for ColumnModel.setRenderer() does not mention that the "this" object will be overridden.

What's worse is, unless you go outside the documentation, you would never even know that it's possible for a renderer to get to the <td> element - which would have been clear, if the <td> element had been part of the callback's argument list.

Animal
11 Dec 2008, 11:03 AM
There is no "real" context which it is "naturally" called in.

Unless you are happy with the context always being the browser window.

ry.extjs
11 Dec 2008, 11:20 AM
couldn't you just use createDelegate?

and what's wrong with 1 line of code (var that = this) to address any scoping issues?

brookd
11 Dec 2008, 11:25 AM
I use

renderer:this.myrenderer.createDelegate(this)

And it works great....

Animal
11 Dec 2008, 11:33 AM
couldn't you just use createDelegate?

and what's wrong with 1 line of code (var that = this) to address any scoping issues?

That's fine when you are hardcoding a new function. But if you are wanting to reference a member function, scope is just nicer:



{
...
MakeRequest: function() {
Ext.Ajax.request({
url: this.actionUrl,
params: this.getParams,
success: this.onRequestSuccess,
failure this.onAjaxFailure,
scope: this // Both callbacks are members of this!
});
}
...
}

mindplay
11 Dec 2008, 1:57 PM
There is no "real" context which it is "naturally" called in.

Unless you are happy with the context always being the browser window.

in all other languages, "this" normally refers to the object of the executing method - that is natural, and usually true in JavaScript too.

Animal
11 Dec 2008, 2:12 PM
No.

We've been through this here HUNDREDS of times.



// Calling a function Mindplay's "natural" way...
function performCallback(fn) {
fn();
}

var foo = {
fn: function() {
alert(this.id);
},
id: 'foo'
};

var bar = {
fn: foo.fn; // references the same function!
// Eeek! What is its "natural" scope now?
id: 'bar'
}

foo.fn(); // will alert "foo"

performCallback(foo.fn); // WILL NOT alert "foo" because "this" will be thw browser window.


What we are talking about is a function POINTER.

The only thing that is specified as a callback, is a pointer to a function somewhere. The user of the pointer can not infer what to use as "this". It can't tell, that whoever passed that function had some object which contained a pointer to that function. In fact any number of objects could contain a pointer to that function, as above.

There is no "natural" scope in Javascript.

evant
11 Dec 2008, 2:30 PM
It could be handy to specify a scope for a renderer, similar to other "callback" type methods.

It's something we could possibly implement in future.

mindplay
11 Dec 2008, 6:23 PM
So what I think just dawned on me, is that the keyword "this" in JavaScript is actually only available in constructors, not in methods.

I never realized that "this" was only available in methods, because it was assigned in the parent scope - the constructor.

So the following does not work:



function Sugar() {
this.flavor = 'sweet';
this.taste = function() {
alert('this tastes ' + this.flavor);
}
}

function Taster(taste) {
this.taste = taste;
this.sample = function() {
this.taste();
}
}

var food = new Sugar();
food.taste();

var test = new Taster(food.taste);
test.sample();


"this tastes undefined", hehe.

Well, I guess then, as evant pointed out, the only solution is to provide a "scope" option for callbacks... which I guess in most other cases is what Ext does.

I guess after 10 years of doing JavaScript, you can still learn something new. I'm starting to get why it's been dubbed "the most misunderstood programming language" ;-)

Animal
12 Dec 2008, 12:52 AM
10 years?

You still have the completely wrong end of the stick!

"this" is available in methods. You just used it there, and it references your object as you expected.

When you call "test.sample", then inside that function, "this" refers to the test object, naturally.

It's just that when you then call "this.taste()" there, that calls the taste function that has been poked in. It calls it using this.taste().

All you have proved there is that the Taster class does not contain a property called "flavor".

"this" is most certainly available in methods.

It is just that if all you have is a POINTER to a function, it has to be imposed upon the function using .call() or .apply()

mjlecomte
12 Dec 2008, 1:54 AM
FYI:
https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Function/apply

mindplay
12 Dec 2008, 6:45 AM
My point with that example was to demonstrate that "this" is not related to the context of the function, but rather to the functioncall - is that not correct?

So even though food.taste() is actually a method of Sugar, the object context is lost because you only pass the pointer to the function - Taster can't know anything about the food object, because no pointer to the object was passed in.

So if I wanted the above example to work, I would have to pass in a "scope" argument as well, and have the Taster class call() the callback with the object as context.

My main field is PHP - I have only been working with Ext for about 6 months, and it's the first framework of this size that I have used. But yes, I have worked with JavaScript for 10 (almost 11) years now, and it's still confusing in some ways - I think the new ways in which people have started using JavaScript in the last couple of years has made it even more confusing, because every framework has different conventions, adds different sugar to the syntax, and uses different techniques. It's been amazing to watch this language grow - without the language itself really changing, it's flexible nature has allowed developers to innovate the way it's used...

I have been programming for over 20 years, in many different languages, but I'm not afraid to admit, JavaScript is the one language that still manages to surprise me.

I was schooled in Pascal originally, so I come from a background of languages that are much more explicit - JavaScript, I find, is a very liberal language; very little syntax, no type safety, no argument validation, no access control, and so on. This is both it's strength and it's weakness, I suppose. For someone with a classical background in more strict languages, it's sometimes troublesome to cope with - I'm used to writing clean code in languages that enforce (or at least encourage) it with their syntax... writing good, clean code in JavaScript takes discipline - which again is both good and bad.

It's good brain exercise though, and I value your help, so thank you :-)

Animal
12 Dec 2008, 7:10 AM
You're kind of getting it.

But there is no such thnig as "being a method of"

An object can hold a reference to a function. But you cannot really say that that function is a method of that object.

It is just a function. Functions in Javascript are just objects to which anyone can hold a reference.

if you call it using



theObject.theRef()


Where the property theRef references a function, then it will use "theObject" as the context. This is what makes it seem as if it was a method "of" that object.

It's just the way it is called.

So if you then do



var theFunc = theObject.theRef;
theFunc();


You assign the variable theFunc to reference the function. Then call that function just using that reference.

The scope in there ("this") will be the browser window. There is no relation back from a function to any class or object.

mindplay
12 Dec 2008, 7:14 AM
I get it. You can even decorate an existing object with additional methods after it's been created - and when invoked, "this" will refer to the object. This sort of stuff just makes my head spin ;-)

Animal
12 Dec 2008, 7:38 AM
Yes you can do that. For example to make an Observable singleton which contains your methods:



MyObservableSingleton = function(){
// private declarations here...
return Ext.apply(new Ext.util.Observable (), {
init: function() {
},

doStuff: function() {
}
});
}();


It pokes some new properties into the created Observable, those properties being your methods of that singleton.

For classes though, you know about all instances of a class (created by using "new" on a function) share the same set of functions by sharing a common prototype object among themselves, and it's the prototype which points to functions when you create a class properly using Ext.extend.

Poking functions into an instance of a class is a kind of one-off override. It's a bit wasteful if you do it a lot, but it can be quite powerful.

I recommend http://www.amazon.com/JavaScript-Good-Parts-Douglas-Crockford/dp/0596517742

mindplay
12 Dec 2008, 8:11 AM
I know Crockford - I think I've seen that book around the office somewhere... I guess I should read it ;-)