pierrespring.com

geek, punk, pussy …

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:

var assure_positive = function (matrix) {
    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 assure_positive = function (matrix) {
    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

matrix.length + 1 > row.lenth

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:

var try_and_alert = function (matrix) {
    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:

var assure_positive = function (matrix) {
    // 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.

Tags:

8 Responses to “Dirty looping in JavaScript”

  1. Interesting method. Just a note on using one-shot functions as you did, I usually saw the function wrapped in parenthesis’ and then executed, rather than the execute then wrap that you have there, so that would look like:

    (function(){
        //stuff
    })();

    I’m not sure if there is a difference between the two as far as browser support goes or anything, but if the other induces errors, you may want to try that.

  2. Pierre Spring says:

    @jordi: Where you put the closing parenthesis does not matter really, it is a question of convention. Crockford demands that you close it the way it’s done in my examples, arguing that the way you do it makes no sense (c.f. video 3 at 29’28”)

    To me both make perfect sense, yet I choose to adhere to the standards proposed by Crockford.

    The only important thing is to have the opening parenthesis, in order to not to have a function expression rather than a function statement. The statement would not be callable.

  3. A Freeman says:

    Why would you reuse the outer loop index for the inner loop, in the first place? Am I missing something here?

    for (i=0;i<matrix.length;i++) {
        var row=matrix[i];
        for (j=0;j<row.length;j++) {
            // stuff happens here
        }
    }
  4. Pierre Spring says:

    Freeman: The point I am trying to make is not to get the enclosing scope of a loop dirty. i and row declaration both get hoisted to the top if you don’t wrap the loop in a function. And this can have unexpected results.

    The assure_positive() is for educational purpose and therefor it is kept very simple.

  5. BObby says:

    Or you could use the let keyword in JavaScript 1.7+, but of course there are problems with this too.

  6. Pierre Spring says:

    @BOobby: the problem with let is that the scope of a variable declared inside a let statement using var is still the same as if it had been declared outside the let statement – such variables still have function scoping.

    In the above example, var row; would still be in the scope of the whole function.

    And of course, let only runs in FF >= 3.0 …

  7. Nice tip, thanks! I also enjoyed the previous posts.

    I guess, another fun thing to do is passing parameters in this nested loop’s function:


    (function(j){
        …
    })(i);

    Thus, the enclosing loop’s i becomes a j in the nested loop.

  8. Jose says:

    Good one! Hoisting is a new concept to me, you helped clarify it. This page is good too:

    http://www.programmerinterview.com/index.php/javascript/javascript-hoisting/