Dirty looping in JavaScript
Due to Function Scope and the hoisting of variable declarations we have to be a bit careful when using loops in JavaScript.
Let’s have a look the following example which is inspired by Douglas Crockford’s 3rd video on JavaScript:
for (var i = 0; i < matrix.length; i += 1) {
// ^ fist declaration of i
var row = matrix[i];
for (var i = 0; i < row.length; i += 1) {
// ^ second declaration of i
if (row[i] < 0) {
throw {
name: 'MatrixError',
message: 'Negative'
};
}
}
}
}
Due to the hoisting of variable declaration, this is the same as:
var row, i;
// ^ declaration got hoisted
for (i = 0; i < matrix.length; i += 1) {
row = matrix[i];
for (i = 0; i < row.length; i += 1) {
// ^ i gets reset to 0
if (row[i] < 0) {
throw {
name: 'MatrixError',
message: 'Negative'
};
}
}
}
}
As you can see both loops share a single variable i. This is certainly not what was intended by the programmer. A first error occurs if
In this case the script will run indefinitely. In many other cases the function will simply produce false positives, as only the first row will be examined. Let’s test this:
try {
assure_positive(matrix);
alert('Ok');
} catch (e) {
alert(e.message);
}
}
try_and_alert([[-1,2,3],[1, -2,-3]]); // -> alerts "Negative"
try_and_alert([[1,2,3],[-1, -2,-3]]); // -> alerts "OK" (false positive)
try_and_alert([[1],[1],[1]]); // -> infinite loop
In addition to that, the row variable is defined in the entire scope of the assure_positive function. For now this is not harmful, but it could lead to future errors when refactoring the coed.
The described problem does not happen in languages with block scope, as each loop has it’s own scope. In order to achieve this in JavaScript, we simply need to wrap each loop into a function:
// wrapper around the loop over the matrix
(function(){
for (var i = 0; i < matrix.length; i += 1) {
var row = matrix[i];
// wrapper around the loop over the row
(function(){
for (var i = 0; i < row.length; i += 1) {
if (row[i] < 0) {
throw {
name: 'MatrixError',
message: 'Negative'
};
}
}
}())
}
}())
}
try_and_alert([[-1,2,3],[1, -2,-3]]); // -> alerts "Negative"
try_and_alert([[1,2,3],[-1, -2,-3]]); // -> alerts "Negative"
try_and_alert([[1],[1],[1]]); // -> alerts "OK"
Every variable that is declared within one of the wrapper functions is only visible in the scope of this function and all it’s sub-functions. It is invisible to the enclosing scope and due to Lexical Scoping (aka Closure) we still have access to the variables defined in the enclosing scopes, such as matrix or row.
Once again, we see how powerful Lexical Scoping is. It gives us the possibility to use functions in order to simulate block scope for our loops.
