JavaScript for C# developers: callbacks (part I)

As I’ve said pretty much from the very start of this series: functions are objects. You can pass them around in variables, pass them into functions as parameters, return them from functions, the whole nine yards. When you pass a function into another where it will be called, it’s generally known as a callback.

(Note: as usual I’m using Firebug in Firefox to run these examples. All browsers have similar development tools built-in or that are available as a download.)

Here’s a simple example. There’s nothing too complicated going on here: all we’re doing is passing a callback into the function that’ll get called once the function’s real work is done.

var printDone = function () {
  console.log("done");
};

var doSomeWork = function (whenDone) {
  // do some work here
  whenDone();
};

doSomeWork(printDone);

So, we’re declaring a function doSomeWork that takes a callback called whenDone as parameter. Once the function has completed, it calls the callback. In the statement at the end, I’m calling doSomeWork passing in the printDone function.

Now, I totally agree that this is a somewhat daft example. After all I could have just added a call to printDone after the call to doSomeWork, but at least it shows the basics. Many times, if not most times, developers won’t formally give the callback function a name like I just did, but instead use an anonymous function declared right there in the call.

var doSomeWork = function (whenDone) {
  // do some work here
  whenDone();
};

doSomeWork(function () {
  console.log("done");
});

Exactly the same effect, but this time we declare the function anonymously as a function literal. You should get used to this style: it’s used universally in all kinds of code.

Now that we’re used to how to declare and use callbacks, let’s up the ante a little. We’re going to write a map method for arrays. A map is a function that applies some kind of operation to each element of an array and returns the resulting values as another array. First of all, let’s write some code that’s specific but works, and then refactor it to make it more general. We’ll start off by writing a function that doubles every element in an array (which we’ll assume to contain just numbers) and returns the resulting array.

var doubler = function (a) {
  var i;
  var result = [];
  for (i = 0; i < a.length; i++) {
    result[i] = 2 * a[i];
  }
  return result;
};

var myArray = [2, 3, 5, 7, 11, 13];
console.log(myArray);
var newArray = doubler(myArray);
console.log(newArray);

Now we’ll refactor it into a method on all arrays. (Note: the JavaScript version in Firefox 4 already has a map method defined on arrays, so I’m calling mine —unimaginatively, perhaps – mapp().)

Array.prototype.mapp = function () {
  var i;
  var result = [];
  for (i = 0; i < this.length; i++) {
    result[i] = 2 * this[i];
  }
  return result;
};

var newArray = myArray.mapp();
console.log(newArray);

Notice that the call to mapp() uses the method pattern of function invocation, and therefore the this variable inside the method points to the array it was called on.

Now, for a generic map method, this is still very specific to doubling elements. But consider the code: we have the stuff that iterates through the array and we have the statement that gets called for each element. It’s this latter part we want to genericize. So, create a special local (nested) function that does the work:

Array.prototype.mapp = function () {
  var i;
  var result = [];

  var process = function (element) {
    return 2 * element;
  };

  for (i = 0; i < this.length; i++) {
    result[i] = process(this[i]);
  }
  return result;
};

And now we see that we can promote this local function into a parameter of the outer function: that is, a callback. And it gives us the form of that callback too: it takes one parameter and returns a new value.

Array.prototype.mapp = function (process) {
  var i;
  var result = [];
  for (i = 0; i < this.length; i++) {
    result[i] = process(this[i]);
  }
  return result;
};

var newArray = myArray.mapp(function (element) {
  return 2 * element;
});
console.log(newArray);

With this last step, the introduction of the callback, the mapp method has become general and we can use it for other transformations or mappings. Supply a callback that does the work, and invoke the mapp method.

var newArray = myArray.mapp(function (element) {
  return "<" + element.toString() + ">";
});
console.log(newArray);

Of course, now that we have a generic mapp() method we can add sanity checks, start to optimize it, or maybe alter the signature of the callback function we accept. For now, let’s make the callback function not only get called with the element in question, but also its index. I also want to call it in such a way that we can pass in the value that the this variable inside the callback should point to.

Array.prototype.mapp = function (process, context) {
  var i, 
      result = [],
      last = this.length;
  for (i = 0; i < last; i++) {
    result[i] = process.call(context, this[i], i);
  }
  return result;
};

var newArray = myArray.mapp(function (element, index) {
  return "<" + index.toString() + ": " + element.toString() + ">";
});
console.log(newArray);

A quick note on the code is in order. I’m calling mapp() without a second parameter. JavaScript will then set the context parameter to undefined. If context is null/undefined, it will default to the global object in the call to call(). (Recall the call method calls the function and sets its this variable to the first parameter.) Also I’m moving the repeated access to the length property out of the loop and saved its value to a local variable: it’s slightly faster.

Next time, we’ll make a slightly different version of the mapp() method to continue our exploration of callbacks.

(This JavaScript callback series: part I, part II, part III.)

Album cover for Welcome to the PleasuredomeNow playing:
Frankie Goes to Hollywood - The Power of Love
(from Welcome to the Pleasuredome)


Loading similar posts...   Loading links to posts on similar topics...

6 Responses

 avatar
#1 santaClara said...
02-Apr-11 2:21 AM

very good & useful

(genericize : ===> make generic; better ===> generalize)

julian m bucknall avatar
#2 julian m bucknall said...
02-Apr-11 8:23 AM

santaClara: Glad you like the articles. As for genericize vs. generalize, I take your point to a certain extent, but I will note that the first is a verbed noun which hasn't made it into the OED yet (why, I have no idea, it seems it should belong; the tech world has been using it for a while). Also the root, generic, is a synonym for general, so at first blush, it would seem that the two verbs are synonyms. Since JavaScript doesn't have generics in the sense of C#, I don't see that there's an appreciable difference.

Cheers, Julian

 avatar
#3 ArShui said...
02-Apr-11 8:06 PM

Hi,

I am a little stupid about "process.call(context, this[i], i)" as the call function don't exist in the "function literal".

Moreover, is there any reference source regarding " If context is null/undefined, it will default to the global object in the call to call()"

julian m bucknall avatar
#4 julian m bucknall said...
03-Apr-11 9:45 AM

ArShui: In answer to your questions:

  1. Functions are objects. Like all objects in JavaScript they can have properties and methods of their own (yes, a function can have a method defined on it). The Function constructor has a prototype object defined on it (as do all functions), and call() is a method defined on that prototype, hence it is defined on all functions (including those that are not newed up via the Function constructor) through the prototype chain. The call() method will execute the function it's called on and set the this variable to the first parameter. All the other parameters are then passed to the function itself.

  2. To find out about the official syntax of JavaScript (including what happens if you call the call() method with a first parameter of null/undefined) you can either read the ECMAScript standard (the 3rd edition of 1999 is here: http://jmbk.nl/Rn27K and it's clause 15.3.4.4) or view the Mozilla developers pages here: http://jmbk.nl/Bn7c5

Cheers, Julian

 avatar
#5 Dave Halsall said...
04-Apr-11 3:45 AM

Hi Julian, great article but a little difficult to read because of the page layout. At least in my current browser (IE7 - not my personal choice, but dictated by corporate policy) everything including code appeared in a very narrow column. It would be much easier if the code snippets did not need scrolling horizontally (or vertically within their box) to read.

julian m bucknall avatar
#6 julian m bucknall said...
01-May-11 4:23 PM

Dave: If you don't have access to a widescreen monitor, I've now added a link in the nav bar at the top to make the second sidebar disappear. That will give you an extra 200px.

Cheers, Julian

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response