Allegro.cc - Online Community

Allegro.cc Forums » Off-Topic Ordeals » Extending Built-In Object With prototype Property (JavaScript)

Credits go to Matthew Leverton and Thomas Fjellstrom for helping out!
This thread is locked; no one can reply to it. rss feed Print
Extending Built-In Object With prototype Property (JavaScript)
bamccaig
Member #7,536
July 2006
avatar

I've encapsulated the XMLHttpRequest/XMLDOM objects into an Ajax wrapper "class" to make requests simpler. It's working pretty well, but there's a major flaw in the design.

For those of you that don't want to read the novel I'll spoil the ending: I either need to add a parent property to the XMLHttpRequest object in IE browsers or I need the XMLHttpRequest object's onreadystatechange callback to refer to the wrapper instance with the this keyword (i.e. I need access to the wrapper instance from the onreadystatechange callback).

Early on I realized that the XMLHttpRequest::onreadystatechange callback isn't aware of the parent object. That is to say, MyAjaxObject::mobjXMLHttpRequest::onreadystatechange has no way to access MyAjaxObject. The callback is essentially used to set properties of the MyAjaxObject instance, as well as trigger "events" during and after the transfer.

I was seemingly able to add a parent property to the XMLHttpRequest object using the prototype property:

    // Extend XMLHttpRequest object with a parent property.
    XMLHttpRequest.prototype.parent = null;

This seemed to work okay for a while, though I don't think it was actually browser safe because of the weird way XMLHttpRequest is created in IE versions. In any case, it seems that it was suddenly no longer valid and I was getting fatal errors so I resorted to a temporary global pointer declared at the top of the script:

    // Temporary MyAjaxObject reference.
    gobjMyAjaxObject = null;

Before a request is made this global pointer is checked and if not yet set it's set to the current instance and a request is made. After the MyAjaxObject instance's properties are set the global pointer is reset to null. If the global pointer is already set (not null) when a request is made it's assumed that a request is in progress and the new request is aborted, signaling failure.

The flaw enforces that only one request can be made at a time, which almost nullifies the point of AJAX. I need a way to add a parent property to the XMLHttpRequest object so that when created as a property of MyAjaxObject it will have a reference to it's parent object (the MyAjaxObject instance).

Does anybody know how to add a property, for example, parent, to the XMLHttpRequest object (that would hopefully work in IE 5.5+ (or below if possible)?

1 function MyAjaxObject()
2 // MyAjaxObject constructor (class).
3 {
4// ...
5 
6 function Request(strFilename, strMethod, blnAsync)
7 // When a request is made the XMLHttpRequest object is created.
8 {
9// ...
10 
11 if(!(this.mobjXMLHttpRequest = createXMLHttpRequest()))
12 return false;
13 
14 // *** SET PARENT PROPERTY ***
15 this.mobjXMLHttpRequest.parent = this;
16 
17// ...
18 }
19 
20// ...
21 
22 function onreadystatechange_callback()
23 /*
24 * When a request is made the onreadystatechange property is assigned
25 * a pointer to this.
26 */
27 {
28// ...
29 
30 /*
31 * this refers to an XMLHttpRequest object and I need to set the
32 * mobjXMLDOM property of the MyAjaxClass instance.
33 */
34 this.parent.mobjXMLDOM = this.responseXML.documentElement;
35 
36// ...
37 }
38 
39// ...
40 }

Matthew Leverton
Supreme Loser
January 1999
avatar

You cannot with IE because it's an ActiveX object, not a native JS object.

I'm not quite sure what you are getting at. This is what I do with my RPC wrapper class:

var rpc = RPC.create();
rpc.responseType = RPC.JSON;
rpc.foo = "bar";
rpc.onLoad = function(json)
{
  alert(this.foo);
};
rpc.get('/foo.html');

That would display "bar" in a dialog box.

bamccaig
Member #7,536
July 2006
avatar

Matthew Leverton said:

I'm not quite sure what you are getting at.

As we know, the XMLHttpRequest::onreadystatechange callback executes whenever the XMLHttpRequest::readyState changes. When the XMLHttpRequest::readyState == 4 (i.e. Complete) the MyAjaxObject::XMLDOM property needs to be set, however, XMLHttpRequest::onreadystatechange doesn't have access to it's MyAjaxObject instance's properties..

    objMyAjaxObject = new MyAjaxObject();

    // MyAjaxObject::Request(string url, string method, bool async);
    objMyAjaxObject.Request("somefile.xml", httpMethod.GET, true);

    /*
     *  Because the request was asynchronous any following code will
     *  continue to execute while somefile.xml is loaded in the
     *  background... The actually processing of XML needs to happen
     *  elsewhere. Elsewhere is the XMLHttpRequest::onreadystatechange callback,
     *  in which the "this" keyword refers to the XMLHttpRequest object and not the
     *  MyAjaxObject instance.
     */

I'm assuming RPC::onLoad is triggered when the XMLHttpRequest::readyState == 4? How do you bind RPC::onLoad to the XMLHttpRequest object...? :-/

Perhaps you could share RPC with me so I can learn from your superior design. ;D

Matthew Leverton
Supreme Loser
January 1999
avatar

Quote:

I'm assuming RPC:: onLoad is triggered when the XMLHttpRequest::readyState == 4? How do you bind RPC:: onLoad to the XMLHttpRequest object...? :-/

Right. But RPC is a wrapper class. For instance:

// inside the RPC.transport constructor
// 'this' is the RPC instance
this.obj = new XMLHttpRequest();  
if (this.obj)
{
  this.obj.onreadystatechange = Delegate.create(this, 'onReadyStateChange');
}

(FYI: For IE, I define the XMLHttpRequest function to return the ActiveX control.)

You're right that you lose the "this" pointer in the onReadyStateChange function if you just do an anonymous function. That's why I use a Delegate to keep track of things. I created this method myself; there may be some other defacto standard way of doing this.

Later on:

RPC.transport.prototype.onReadyStateChange = function()
{
  // this.obj points to the XMLHttpRequest object.

  // eventually:
  else if (this.responseType == RPC.JSON)
  {
    eval("var json = " + this.obj.responseText);
    this.onLoad(json);
  }
}

My Delegate class looks like:

1/*
2 Copyright 2006-07 by Matthew Leverton
3
4 Delegate: a static class to help keep events tied to an object.
5*/
6var Delegate = {
7 objects: new Array(),
8
9 /*
10 (object) ptr: the object the event should be tied to
11 (string) func: the name of the function that should be called; it must reside in the ptr object.
12 */
13 create: function(ptr, func)
14 {
15 if (!ptr._delegates)
16 {
17 ptr._delegates = new Array();
18 Delegate.objects[Delegate.objects.length] = ptr;
19 }
20
21 return !ptr._delegates[func] ? (ptr._delegates[func] = function(event) { return ptr[func](event); }) : ptr._delegates[func];
22 },
23
24 /*
25 Returns a delegate already created. Note that subsequent calls to 'create' are the
26 same as calling get, so this method really doesn't ever _need_ to be used.
27 */
28 get: function(ptr, func)
29 {
30 return (!ptr._delegates || !ptr._delegates[func]) ? false : ptr._delegates[func];
31 },
32
33 /*
34 Destroys the delegate (but not the actual function). It can be useful in removing
35 small memory leaks in some browsers, but doesn't have to be called.
36 */
37 destroy: function(ptr, func)
38 {
39 if (!ptr._delegates || !ptr._delegates[func]) return false;
40
41 ptr._delegates[func] = null;
42 },
43
44 /*
45 Destroys all the delegates. (See the destroy method.)
46 */
47 destroyAll: function()
48 {
49 for (var i in Delegate.objects)
50 {
51 for (var j in Delegate.objects<i>._delegates)
52 {
53 Delegate.objects<i>._delegates[j] = null;
54 }
55 }
56 }
57};

bamccaig
Member #7,536
July 2006
avatar

Wow, some syntax I've never seen before... Cool. :) Anyway, I'm not exactly sure I understand it all. My understanding of events in JavaScript is more or less hit or miss. I'm not sure I understand your Delegates class either... I'm going to have to assume much of RPC and Delegates are still missing and that's why. ;D

Today a coworker and I (it didn't really require both of us ;D) managed to set a parent property on the XMLHttpRequest object which works in IE7.

    function createXMLHttpRequest(parent)
    {
    //  ...

        // Set the parent property.
        if(objXMLHttpRequest != null)
            objXMLHttpRequest.parent = parent;

        // Return the object.
        return objXMLHttpRequest;

    //  ...
    }

Unfortunately, IE6 is complaining that the parent property isn't implemented.

    // Default callback.
    this.mobjXMLHttpRequest.onreadystatechange = this.onreadystatechange_callback;

Perhaps you can explain the difference between what Delegates.create() returns and a function pointer as assigned in the above code...? :-/

It looks like you're creating a function and storing it in an array, instance._delegates[]. So I'm guessing that when you call these functions you actually call the Delegates.get() method which uses the instance pointer and _delegates index (ptr, func) to access the function instance...
Delegates.get(objMyAjaxObject, "onReadyStateChange")();
Could you explain it in more detail please? And I still don't see how you bind RPC::onLoad to the XMLHttpRequest object... :-/ Unless your onReadyStateChange function calls onLoad... :-/

Thomas Fjellstrom
Member #476
June 2000
avatar

Personally, why do it all yourself, when someone else has done it?

http://www.prototypejs.org/

It supports IE, Firefox, and works well in Konqueror as well.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Matthew Leverton
Supreme Loser
January 1999
avatar

I was going to recommend prototype.js too. It is well written and useful. I just don't use it because I hate other people's code. ;D That, and it adds some bloat if you don't use it all.

Anyway, I'll post back again when I have some time to give a deeper explanation of what my Delegate class does.

Thomas Fjellstrom
Member #476
June 2000
avatar

I really like prototype. It might be a little bloat, but if you look into all the api provides, MUCH of it is very useful in a dynamic site.

I've even made widget classs based on the prototype Object/Class stuff, and its worked out very well.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Matthew Leverton
Supreme Loser
January 1999
avatar

The Delegate class I provided above is complete. Here is a simple example (not using it):

1<html>
2
3<head>
4<title>Test</title>
5 
6<script type="text/javascript">
7<!--
8 function Foo()
9 {
10 this.randomVariable = "Hello, World!";
11 };
12
13 Foo.prototype.setup = function()
14 {
15 var self = this; // 'this' will be lost in the anonymous function
16
17 document.getElementById("btn1").onclick = this.bar;
18 document.getElementById("btn2").onclick = function(event)
19 {
20 self.bar(event);
21 };
22 };
23
24 Foo.prototype.bar = function(event)
25 {
26 if (typeof this.randomVariable == "undefined")
27 alert("I don't know who I am!");
28 else
29 alert(this.randomVariable);
30 };
31
32 var foo = new Foo();
33
34 function body_onLoad()
35 {
36 foo.setup();
37 };
38
39//-->
40</script>
41 
42</head>
43 
44<body onload="body_onLoad()">
45
46 <input id="btn1" type="button" value="No Delegate" />
47
48 <input id="btn2" type="button" value="Delegate" />
49
50</body>
51 
52</html>

When button one is pressed, you'll see that the reference of the Foo object is lost. But when button two is pressed, the reference is maintained by an anonymous function, which I call the "delegate."

All my Delegate class does is make it easy to wrap all of those inline anonymous functions into one delegate pointer. (That is, maybe you have multiple events that all want to call the same method in an object.)

Now what you should be doing is creating a wrapper around your XMLHttpRequest object, similar to my object Foo. Then your onreadystatechange, would look like:

1function Foo()
2{
3 var self = this;
4 this.req = new XMLHttpRequest();
5 this.req.onreadystatechange = function()
6 {
7 if (self.req.readyState == 4) self.onLoad( xmlData /* whatever */ );
8 };
9};
10 
11Foo.prototype.onLoad = function(xmlData)
12{
13 alert("Hey: " + xmlData);
14 
15 // maybe call a user callback, if set
16 
17 // notice that we can now access arbitrary variables:
18 alert(this.randomProperty);
19};
20 
21Foo.prototype.get = function(url)
22{
23 this.req.open(url);
24};
25 
26// Usage:
27 
28var foo = new Foo();
29foo.randomProperty = "blah";
30foo.get("/whatever.html");

Obviously you'll need to fill in the blanks, but that is the general idea of how to build a wrapper class around XMLHttpRequest.

Dustin Dettmer
Member #3,935
October 2003
avatar

onreadystatechange is a very ugly way to handle this situation. Much cleaner to do it this way:

var obj = new MyAjaxObject();

obj.open("POST", url, false);
obj.send();

if(obj.status != 200) {
  alert("Server connection unavailable");
  return;
}

...

Thomas Fjellstrom
Member #476
June 2000
avatar

Quote:

onreadystatechange is a very ugly way to handle this situation. Much cleaner to do it this way:

And is that async?

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Dustin Dettmer
Member #3,935
October 2003
avatar

Right, async is what makes it so ugly. You want to avoid async.

Thomas Fjellstrom
Member #476
June 2000
avatar

Uh, the point to XMLHTTP is to be async. I don't want a js script hanging the browser while its downloading something.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Dustin Dettmer
Member #3,935
October 2003
avatar

Who said it was going to hang the browser?

Matthew Leverton
Supreme Loser
January 1999
avatar

Quote:

You want to avoid async.

That's absolutely funny. You call it a MyAjaxObject(). Let's remind ourselves what the acronym Ajax stands for: Asynchronous JavaScript and XML.

If you don't do it asynchronously, the JS interpreter will pause and hang until it's finished. Whether or not the browser remains your friend, depends on the implementation. So your suggestion is, quite frankly, terrible.

bamccaig
Member #7,536
July 2006
avatar

Thanks a lot, Matthew! The following appears to be working! ;D

1 function MyAjaxObject()
2 // MyAjaxObject constructor (class).
3 {
4// ...
5 
6 function Request(strFilename, intMethod, blnAsync, intResponseType)
7 // When a request is made the XMLHttpRequest object is created.
8 {
9 var _self = this;
10 
11// ...
12 
13 if(!(this.mobjXMLHttpRequest = createXMLHttpRequest()))
14 return false;
15 
16// ...
17 
18 this.mobjXMLHttpRequest.onreadystatechange = function()
19 {
20 _self.onreadystatechange_callback();
21 }
22 
23// ...
24 }
25 
26// ...
27 
28 function onreadystatechange_callback()
29 /*
30 * When a request is made the onreadystatechange property is assigned
31 * a pointer to this.
32 */
33 {
34// ...
35 
36 if(this.mobjXMLHttpRequest.readyState == ajaxReadyState.Complete)
37 {
38 if(this.mobjXMLHttpRequest.status == httpStatus.OK)
39 {
40// ...
41 
42 /*
43 * this refers to an XMLHttpRequest object and I need to set the
44 * mobjXMLDOM property of the MyAjaxClass instance.
45 */
46 this.mobjXMLDOM = this.mobjXMLHttpRequest.responseXML.documentElement;
47 
48// ...
49 }
50 }
51 
52// ...
53 }
54 
55// ...
56 }

I'd appreciate confirmation that I'm doing things correctly... :-/;D I'd also appreciate seeing RPC class in it's entirety. ;D

Thanks also, Thomas Fjellstrom, for the link to Prototype library or whatever... ;D That looks like it can make JS a little less painful. As Matthew Leverton said I'm also a little partial to my own code, but that looks worthwhile to try (and probably learn from).

Dustin Dettmer
Member #3,935
October 2003
avatar

Quote:

If you don't do it asynchronously, the JS interpreter will pause and hang until it's finished. Whether or not the browser remains your friend, depends on the implementation. So your suggestion is, quite frankly, terrible.

Hm, I ran some tests and it looks like the JS interpreter does pause. Thats good to know then. Looks like the _self system is probably the best answer. I try to avoid doing that because of the IE memory leak bug, but it looks like thats the only portable way to get it done.

bamccaig
Member #7,536
July 2006
avatar

Dustin Dettmer said:

I try to avoid doing that because of the IE memory leak bug,...

Care to elaborate? :):-/:'(

Dustin Dettmer
Member #3,935
October 2003
avatar

I forget the specifics but I believe in this scenario:

function foo()
{
  var i;

  function bar()
  {

  }

  bar();
}

The varaible 'i' is never cleaned up.

bamccaig
Member #7,536
July 2006
avatar

In my example, would _self contain the entirely of the class or merely a memory address as a C++ pointer does? In other words, if this bug applies is a lot of data not being cleaned up or only a little bit? :-/ And do you know of a way to manually clean up _self without breaking the functionality it provides...?

For example:

1 function Request(strFilename, intMethod, blnAsync, intResponseType)
2 // When a request is made the XMLHttpRequest object is created.
3 {
4 var _self = this;
5 
6// ...
7 
8 if(!(this.mobjXMLHttpRequest = createXMLHttpRequest()))
9 return false;
10 
11// ...
12 
13 this.mobjXMLHttpRequest.onreadystatechange = function()
14 {
15 _self.onreadystatechange_callback();
16 }
17 
18 /*
19 * Does the onreadystatechange callback need this value set after the
20 * declaration or might this cleanup the leak? :-/
21 */
22 delete _self;
23 
24// ...
25 }

My AJAX wrapper is still working even after the delete statement. Do you think that could take care of the leak? :-/

Matthew Leverton
Supreme Loser
January 1999
avatar

Quote:

I'd appreciate confirmation that I'm doing things correctly... :-/;D I'd also appreciate seeing RPC class in it's entirety. ;D

It looks fine. Basically my RPC class looks like:

var rpc = RPC.create();
rpc.addField("foo", "bar");
rpc.setResponseType(RPC.JSON); // .HTML or .XML
rpc.onLoad = function(json)
{
  alert(json.someProp);
}
rpc.get(url);  // or .post();

The implementation is straightforward. When adding a field, I use encodeURIComponent() on the key and value like: data += encodeURIComponent(key) + "=" + encodeURIComponent(value); That goes in the get() / post() methods. (or Request in your case.)

The onLoad call back is only called on success. Otherwise onError is called. (Note they are both user defined functions.) If the responseType is JSON, then it automatically gets eval()'d before calling the onLoad(). For XML, the XML doc is passed. For HTML, it's the response string.

bamccaig
Member #7,536
July 2006
avatar

Matthew Leverton said:

rpc.addField("foo", "bar");

I never thought to encapsulate the content management as well. I simply had (;)) a property for content which was null by default. Now I have Append(). 8-)

Matthew Leverton said:

Otherwise onError is called.

I'm curious how you know an error occurred. Is that fired when XMLHttpRequest.status != 200? :-/

On a side note, perhaps you can shed some light on something for me. During testing I was having some trouble figuring out why certain things weren't happening... Eventually I traced it back to XMLHttpRequest.status == 0... :-/ WTF!? I'm still not sure why, but I've added another "enumeration" to my check because the transfer seems to be successful anyway (status == OK || status == ZERO). I'm using HTTPS which I assume is responsible for it... :-/

Dustin Dettmer
Member #3,935
October 2003
avatar

Quote:

would _self contain the entirely of the class or merely a memory address as a C++ pointer does?

My understanding is that it is a pointer. You can probably call delete manually. delete is compatible back to Javascript 1.2 apparently. I have idea where IE is on delete however...

Quote:

In other words, if this bug applies is a lot of data not being cleaned up or only a little bit?

The whole class instance and anything it references (expired nodes, temporary functions, scopes from function calls). Javascript is particularly memory inefficient.

Go to: