If you have ever experienced this JavaScript problem, you're not alone - it trips up many people and can be tricky, at first, to understand exactly how to fix it. Let's consider this example:
1var array = [ ... ]; // An array with some objects
2for( var i = 0; i < array.length; ++i )
3{
4 $.doSthWithCallbacks( function() {
5 array[i].something = 42;
6 });
7}
While this code looks perfectly fine, it shows the misunderstanding of a very basic JavaScript concept. Now, if you're well-versed in Javascript, this error should be easy enough to spot. But for most folks, this isn't case - some people can literally spend hours trying to figure out why their code isn't working.
Remember those first JavaScript tutorials you read, where it said that JavaScript is asynchronous? This means that under some circumstances code might not be executed sequentially. This is usually the case when using internal APIs that depend on an external event. For example, processing a response after an HTTP request is completed or after some other processing is done.
So what happens then, is that the doSthWithCallbacks
(general expression for all JavaScript function that use a callback) schedules
the callback function to be executed at a later stage. But the for
loop isn't
just scheduling one callback. It's scheduling an array.length
worth of
callbacks and they most certainly won't be completed within the same for
loop
iteration. Each of those callbacks will be executed at an unpredictable time
later on, when multiple for
iterations have gone through, the value if i
is
different and multiple other callbacks have also been scheduled.
Usually, the callbacks aren't executed until the for
loop has completed, at
which point i
is exactly equal to array.length - 1
. So, every time any of
the callbacks is executed it will be modifying the last value of the array
instead of the value of the for
loop iteration it was scheduled on. Of course,
as I said, it's unpredictable when the callbacks will be executed and depends on
multiple factors the JavaScript interpreter used, the function invoking the
callbacks and it's input data. An example is an HTTP request with a success
callback that won't be executed before the server sends a response, which could
be any time interval between several milliseconds and several minutes.
I am going to present you with two solutions on how to go around the problem. Both are very efficient in terms of performance and memory consumptions. The first method is easier to understand, but requires the definition of a function in an upper scope, which makes the code a bit less readable since you have to look up the function. The second solution is my personal favorite but is harder to understand, especially when you see such constructs for the first time. There are other solutions, but these at the moment are the fastest and are supported in all major browsers and JavaScript interpreters.
Essentially both methods do the same task but in different ways. What they do is
creating a separate callback function with their own copy of the value of i
in
a scope only available to them.
This method is relatively easy to understand and that is why I won't be covering
it in much detail. This isn't the case with inline closures, so I will cover
that in depth. The callbackClosure
function returns a function that invokes
the actual callback with an explicit copy of i
as an argument.
1var array = [ ... ]; // An array with some objects
2
3function callbackClosure(i, callback) {
4 return function() {
5 return callback(i);
6 }
7}
8
9for( var i = 0; i < array.length; ++i )
10{
11 API.doSthWithCallbacks( callbackClosure( i, function(i) {
12 array[i].something = 42;
13 }) );
14}
Since each function declares it's own scope, and i
has a base atomic type
(int
) it is not passed as a reference, but rather as a copy (unlike objects)
which ensures that the actual callback will be executed against the correct
value.
This brings us to my most favorite JavaScript hack. This is done by declaring a self called anonymous function, which generally looks like this:
1(function() {
2 // Something declared here will only be available to the function below.
3 // Code here is executed only once upon the creation of the inner function
4 return function(callbackArguments) {
5 // Actual callback here
6 };
7})(); // The last brackets execute the outer function
Note that the outer function is only used for encapsulating the inner function,
and creating a separate variable scope for the inner function. Also, the outer
function returns a value of type Function
which is the exact type a callback
should be. So, applying this to the previous example we arrive here:
1var array = [ ... ]; // An array with some objects
2for( var i = 0; i < array.length; ++i )
3{
4 API.doSthWithCallbacks( (function() {
5 var j = i; // j is a copy of i only available to the scope of the inner function
6 return function() {
7 array[j].something = 42;
8 }
9 })() );
10}
If, for example, you have to do some asynchronous processing, and there should
be some aggregate code that should only be run after all the callbacks have been
completed, all you need to do is know how many callback you have scheduled and
count how many of those were completed. If count
is equal to length
that
means that you are currently processing the last callback.
1var array = [ ... ]; // An array with some objects
2var count = 0, length = array.length;
3for( var i = 0; i < array.length; ++i )
4{
5 API.doSthWithCallbacks( (function() {
6 var j = i; // A copy of i only available to the scope of the inner function
7 return function() {
8 array[j].something = 42;
9
10 ++count;
11 if( count == length ) {
12 // Code executed only after all the processing tasks have been completed
13 }
14 }
15 })() );
16}
Now, at this point it's easy to get confused. Is the ++count
operation atomic?
A Race Condition could occur and the code might be
executed multiple times or, worse, not executed at all. Some consider something
like a mutex or a semaphore. But this isn't right.
While JavaScript is asynchronous, it's not multithreaded. In fact, while it's impossible to predict when a callback will be executed, it is guaranteed that a Race Condition will not occur since JavaScript only runs in a single thread. (As a side note, that doesn't mean there isn't a way to run multiple threads in JavaScript. See Web Workers API for the Web kind of JavaScript).
ECMAScript 6 introduces the let
keyword which allows you to declare a variable
scoped to the nearest enclosing block and not global like var
does. Thus the
closure problem can be solved simply by replacing var
with let
:
1var array = [ ... ]; // An array with some objects
2for( let i = 0; i < array.length; ++i )
3{
4 $.doSthWithCallbacks( function() {
5 array[i].something = 42;
6 });
7}
Neat, isn't it? If you can use ES6 your code can be made to look so much better without the need to polute it with immedately invoked anonymous inline function calls just to create a new scope.
I've prepared a Practice Problem demonstrating the issue and if you would like, you can give it a try. When attempting it, only edit the code within the designated section. There are multiple ways to do it, but one of the best is using the second technique I showed you.