Forum Moderators: open

Message Too Old, No Replies

JavaScript Scope

or, Why Won't My AJAX Work?

         

cmarshall

5:28 pm on Sep 13, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



META: I posted this in our internal wiki today, and thought it might have relevance here. The audience is seasoned C++ programmers, so I apologize if anything seems obtuse.

I was chatting with someone recently, and realized that the issue of scope in asynchronous programming is the biggest impediment to implementation.
I've been writing drivers and asynch code since I was knee-high to a grasshopper, so I never even think about this, but I realize that, for many people, their first encounter with a scopeless callback can be like running into a brick wall.

The Awful Truth: JavaScript is a Lash-Up

"You're a mean man to say that, and Tim O'Reilly is gonna come after you!"

Sorry. This is a basic fact of life. JavaScript is a very clever and well-done lash-up, but a lash-up nonetheless. Let me demonstrate.

I use this bit of code to open people's eyes to the stark, unvarnished truth:

<script type="text/javascript">
// This is a function.
function some_function () { alert ( "Hello, World!" ); }
// This is a function "pointer."
var function_ptr = some_function;
// This is what is inside a function "pointer"
alert ( function_ptr );
</script>

Yup. The entire Web 2.0 is based on a language that behaves like that. Scary, huh? When they say JavaScript is an interpreted language, they mean it.

I just wanted to get people settled that they simply cannot expect the same level of low-level sophistication and support from JavaScript that they would from C++ or Pascal.

Callback Scope
This is a problem in most languages, not just JavaScript. However, many languages come with constructs or standard libraries that make it easier to deal with threading and asynchronous operation. I do know that there is a lot of work going on in the JavaScript community to establish common frameworks for this stuff. So far, what I've seen reminds me more of Rube Goldberg devices than useful code, but I'm picky.

The way asynchronous execution works, is that you start a process going, and give it a callback. You say "Go do your stuff, and call me when you're done."

Global Scope
These examples use the JavaScript TimeOut [w3schools.com] functionality:

<script type="text/javascript">
// This starts the asynchronous process.
// It tells the browser to wait 3 seconds, then call the "Callback" function.
function StartProcess() {
setTimeout ( Callback, 3000 );
}
// This function is called when the asynchronous operation is complete.
function Callback() {
alert("I'm done!");
}
// Immediately start the process
StartProcess();
</script>

This is simple enough. The asynchronous process is the standard, JavaScript TimeOut functionality, where it just waits around, then calls you when it is done. Its only real use is as a delay mechanism or a way to catch timeouts (if an operation is taking too long, you can have the timeout trigger an error handler). The nice thing about true asynchronous operation (like this example) is that it returns control to the main context. The asynchronous operation is chugging away in the background while you do your thing in the foreground.

You can even have multiple operations going on, like so:

<script type="text/javascript">
// This function will set three independent processes going
function StartProcess() {
document.getElementById('display_id').innerHTML="Working on it...";
setTimeout ( Callback3s, 3000 );
setTimeout ( Callback5s, 5000 );
setTimeout ( Callback7s, 7000 );
}
// This callback is triggered after 3 seconds.
function Callback3s() {
document.getElementById('display_id').innerHTML="I'm done after 3 Seconds!<br />";
}
// This callback is triggered after 5 seconds.
function Callback5s() {
document.getElementById('display_id').innerHTML+="I'm done after 5 Seconds!<br />";
}
// This callback is triggered after 7 seconds.
function Callback7s() {
document.getElementById('display_id').innerHTML+="I'm done after 7 Seconds!<br />";
document.getElementById('display_id').innerHTML+='<a href="javascript:StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:StartProcess()">Start Counting</a></div>

Now, a simpler and more flexible way to do this might be:

<script type="text/javascript">
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
function StartProcess() {
document.getElementById('display_id').innerHTML="Working on it...<br />";
setTimeout ( Callback_Complete, 7000 );
for ( var t=1000; t < 7000; t += 1000 ){
setTimeout ( Callback, t );
}
}
// This callback is triggered by the loop.
function Callback() {
document.getElementById('display_id').innerHTML+="I'm being called after 1 second!<br />";
}
// This callback is triggered after we're done.
function Callback_Complete() {
Callback();
document.getElementById('display_id').innerHTML+='<a href="javascript:StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:StartProcess()">Start Counting</a></div>

That's nice, but it would be nice if we could have a count of how many seconds have elapsed:

<script type="text/javascript">
// This is a global variable that will be incremented in each loop.
varg_seconds = 0;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
function StartProcess() {
document.getElementById('display_id').innerHTML="Working on it...<br />";
setTimeout ( Callback_Complete, 7000 );
for ( var t=1000; t < 7000; t += 1000 ){
setTimeout ( Callback, t );
}
}
// This callback is triggered by the loop.
function Callback() {
g_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+g_seconds+" seconds!<br />";
}
// This callback is triggered after we're done.
function Callback_Complete() {
Callback();
document.getElementById('display_id').innerHTML+='<a href="javascript:StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:StartProcess()">Start Counting</a></div>

Now, to make things even more efficient, let's trigger these as a cascade:
<script type="text/javascript">
varg_seconds = 0;
// This is the total number of seconds to count.
varg_countmax = 7;
// This function will show a new prompt every second, while it counts to g_countmax seconds.
// After it is complete, it will display the "Start Over" link.
function StartProcess() {
document.getElementById('display_id').innerHTML="Working on it...<br />";
setTimeout ( Callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
function Callback() {
g_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+g_seconds+" seconds!<br />";
if ( g_seconds < g_countmax ) {// If we haven't finished,
setTimeout ( Callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
Callback_Complete();
}
}
// This function is called after we're done.
function Callback_Complete() {
document.getElementById('display_id').innerHTML+='<a href="javascript:StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:StartProcess()">Start Counting</a></div>

That works nicely. It's fairly efficient, flexible code.

Note that we used two global variables to denote the counter and the maximum count (g_seconds and g_countmax). The callback knows about the global scope, so it can get at them.

Remember when I showed you what a function "pointer" consists of? It is a string, with the ENTIRE FUNCTION crammed into it. It is not a pointer in any sense of the word. This means that when you call a function via a function "pointer," it is completely devoid of context or scope.

It floats alone, in a formless sea. It has no roots.

The only context available to a callback is the global context. Keep this in mind for the next conversation.

Object Scope
"Gee," you say. "That's sooo five minutes ago! Can't you use objects, so you can have multiple counters going?"

Fair enough. Let's "objectify" this:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
setTimeout ( this.Callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function () {
this._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+this._c_seconds+" seconds!<br />";
if ( this._c_seconds < this._c_countmax ) {// If we haven't finished,
setTimeout ( this.Callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
this.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">This Won&apos;t Work</a></div>


Oh dear. It doesn't work. You only get one iteration, and it displays "NaN," which is JavaScript for "huh?".

Now, let me show you something that does work, and we can discuss the reason why after that:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
setTimeout ( this.Callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function () {
g_counter._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+g_counter._c_seconds+" seconds!<br />";
if ( g_counter._c_seconds < g_counter._c_countmax ) {// If we haven't finished,
setTimeout ( g_counter.Callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
g_counter.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">This Will Work</a></div>

What's the difference between these two implementations, and why does one work, and the other one not?

The difference is that, in the second implementation callback, we replaced the "this" operator with a reference to the global object (g_counter). "this" has no meaning in a context-free callback.

That kinda defeats the whole purpose of having objects, eh?

Maybe we can use all that fancy inline function stuff to make it work. Let's try this:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
// This allows us to pass in a parameter, which will refer back to us (C++ programmers will find this familiar).
var the_function_callback = function(){this.Callback(this)};

setTimeout ( the_function_callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function (in_object) {
in_object._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+in_object._c_seconds+" seconds!<br />";
if ( in_object._c_seconds < in_object._c_countmax ) {// If we haven't finished,
var the_function_callback = function(){in_object.Callback(in_object)};
setTimeout ( the_function_callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
in_object.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Start Over</a>';
}

var g_counter = new CJSCounter;// Create an instance of this class.
</script>
<div id="display_id"><a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">This Still Won&apos;t Work</a></div>


Nope. We get an error as soon as the first callback comes up. We didn't even get the NaN message this time.

This is because of the problem with the function "pointer" I mentioned before. Let's have the script display what it is handing the TimeOut as a callback:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
var the_function_callback = function(){this.Callback(this)};
alert(the_function_callback);
setTimeout ( the_function_callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function (in_object) {
in_object._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+in_object._c_seconds+" seconds!<br />";
if ( in_object._c_seconds < in_object._c_countmax ) {// If we haven't finished,
var the_function_callback = function(){in_object.Callback(in_object)};
setTimeout ( the_function_callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
in_object.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Start Over</a>';
}

var g_counter = new CJSCounter;// Create an instance of this class.
</script>
<div id="display_id"><a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">This Still Won&apos;t Work</a></div>


As you can see, we are handing the TimeOut a string that describes a function to be executed in the global scope. "this" is specified at the time the callback is made, and, at the time the callback is made, "this" has no meaning, as we are in the global scope.

Okay, we can make it work, but we have to return to the global scope again:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
var the_function_callback = function(){g_counter.Callback(g_counter)};
setTimeout ( the_function_callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function (in_object) {
in_object._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+in_object._c_seconds+" seconds!<br />";
if ( in_object._c_seconds < in_object._c_countmax ) {// If we haven't finished,
var the_function_callback = function(){in_object.Callback(in_object)};
setTimeout ( the_function_callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
in_object.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:var g_counter = new CJSCounter;g_counter.StartProcess()">Okay, Now This Will Work</a></div>


Now, of course, the problem here is that we are back to being able to only work with one of these at a time.

We can improve the "objectness" of the function slightly:

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function () {
document.getElementById('display_id').innerHTML="Working on it...<br />";
var this_object = this; // Remember that this frees the object from the global scope.
var the_function_callback = function(){this_object.Callback(this_object)};
setTimeout ( the_function_callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function (in_object) {
in_object._c_seconds += 1;
document.getElementById('display_id').innerHTML+="I'm being called after "+in_object._c_seconds+" seconds!<br />";
if ( in_object._c_seconds < in_object._c_countmax ) {// If we haven't finished,
var the_function_callback = function(){in_object.Callback(in_object)};
setTimeout ( the_function_callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
in_object.Callback_Complete();
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function () {
document.getElementById('display_id').innerHTML+='<a href="javascript:(new CJSCounter).StartProcess()">Start Over</a>';
}
</script>
<div id="display_id"><a href="javascript:(new CJSCounter).StartProcess()">This Will Work</a></div>


Now, if you were paying attention, you would say something like "Wait a minute! You refer to 'in_object' in the function, but you told us that function pointers don't work!

Actually, that is correct. However, object parameters do work. You can't pass "this" as an object parameter, but you can pass a variable to which you had previously assigned the value of "this" (this_object). This will be propagated throughout the daisy-chain of callbacks.

In a callback, "this" has no meaning, but an object pointer does have meaning. This works in AJAX callbacks as well as in TimeOut callbacks.

This means that you can, indeed, instantiate a number of discrete, independent objects, and they can each have their own contexts.

However, they can't have "this" in a callback. The context needs to be passed in via an object pointer.

cmarshall

8:32 pm on Sep 13, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Okay, just to prove how nicely it works, here's kind of a hairy example. Note that I added a
delete(in_object)
to reduce memory leaks.

<script type="text/javascript">
// Constructor (empty).
function CJSCounter() {};

// This is the ID of the container
CJSCounter.prototype._c_id = '';
// This is an incrementing counter
CJSCounter.prototype._c_seconds = 0;
// This is the total number of seconds to count.
CJSCounter.prototype._c_countmax = 7;
// This function will show a new prompt every second, while it counts to seven seconds.
// After it is complete, it will display the "Start Over" link.
CJSCounter.prototype.StartProcess = function (in_id) {
this._c_id = in_id;
document.getElementById(this._c_id).innerHTML="Working on it...<br />";
var this_object = this; // Remember that this frees the object from the global scope.
var the_function_callback = function(){this_object.Callback(this_object)};
setTimeout ( the_function_callback, 1000 );// We simply start the process going.
}
// This callback is triggered by the loop.
CJSCounter.prototype.Callback = function (in_object) {
in_object._c_seconds += 1;
document.getElementById(in_object._c_id).innerHTML+="I'm being called after "+in_object._c_seconds+" seconds!<br />";
if ( in_object._c_seconds < in_object._c_countmax ) {// If we haven't finished,
var the_function_callback = function(){in_object.Callback(in_object)};
setTimeout ( the_function_callback, 1000 );// Start the next asynchronous process going.
}
else {// We're done. Stick a fork in us.
in_object.Callback_Complete(in_object._c_id);
delete ( in_object );
}
}
// This function is called after we're done.
CJSCounter.prototype.Callback_Complete = function (in_id) {
document.getElementById(in_id).innerHTML+='<a href="javascript:(new CJSCounter).StartProcess(\''+in_id+'\')">Start Over ('+in_id+')</a>';
}
// This just spits out five independent <div> elements, each with its own object.
for ( var id=1; id < 5; id++ ) {
document.write('<div id="display_'+id+'"><a href="javascript:(new CJSCounter).StartProcess(\'display_'+id+'\')">Start Count (display_'+id+')</a></div> ');
}
</script>