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.)
Now playing:
Frankie Goes to Hollywood - The Power of Love
(from Welcome to the Pleasuredome)
6 Responses
#1 santaClara said...
02-Apr-11 2:21 AMvery good & useful
(genericize : ===> make generic; better ===> generalize)
#2 julian m bucknall said...
02-Apr-11 8:23 AMsantaClara: 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
#3 ArShui said...
02-Apr-11 8:06 PMHi,
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()"
#4 julian m bucknall said...
03-Apr-11 9:45 AMArShui: In answer to your questions:
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.
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
#5 Dave Halsall said...
04-Apr-11 3:45 AMHi 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.
#6 julian m bucknall said...
01-May-11 4:23 PMDave: 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.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response