Forum Moderators: open
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>
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>
<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>
<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>
<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>
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't Work</a></div>
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't Work</a></div>
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't Work</a></div>
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>
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>
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.
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>