5

I cannot understand why this code is behaving as it is:

for (var i = 1; i < 3; i++) {
    var j;
    if (!j) {
        j = 1;
    } else {
        alert("why are we here? j shouldn't be defined but it's " + j);
    }
}

(jsFiddle)

If I set j to null and check for null, it works the way I think it should.

This isn't how Java, C#, C++, etc. work, hence, the confusion.

Ry-
  • 218,210
  • 55
  • 464
  • 476
Yamcha
  • 1,264
  • 18
  • 24

3 Answers3

13

This is because variables in JavaScript are scoped to functions (or the global scope if not within a function). A var is not scoped inside of a loop, and curly braces do not defined a new closure. Basically, the value of j persists after the loop body, and the var does not re-define j as undefined. This is why explicitly setting var j = null; has the expected effect.

Example 1:

A good way to think about this is, any time you declare a variable with var, like this.

function someFunc() {
    for(var i = 0; i < 3; i++){
        var j;
    }
}

The interpreter hoist the variable declarations like so.

function someFunc() {
    var i;
    var j;
    for(i = 0; i < 3; i++){
    }
}

Notice that since the var j declaration is hoisted to the top of the function, the declaration actually does nothing within the loop.

Example 2:

However, if you were to initialize the variable with null like this.

function someFunc() {
    for(var i = 0; i < 3; i++){
        var j = null;
    }
}

It would be interpreted like this.

function someFunc() {
    var i;
    var j;
    for(i = 0; i < 3; i++){
        j = null;
    }
}

Notice how for every loop, j is set to null.

ES6 let keyword:

There is a keyword in ES6 which will create a scope in a loop like this, it is the let keyword. Keep in mind that browser support for the let keyword is poor at this point.

for (var i = 1; i < 3; i++) {
    let j;
    if (!j) {
        j = 1;
    } else {
        alert("why are we here? j shouldn't be defined but it's "+ j);
    }
}
Community
  • 1
  • 1
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
4

The first time your for loop executes the body of the loop, j is undefined and thus your code sets j=1. On the subsequent iterations of the loop, j is already defined and set to 1 so it goes into your else clause as would be expected.

This occurs because variables defined with var in Javascript are function scoped, not block scoped and if not inside a function, then they are global. So, there is only one variable j in your jsFiddle and each iteration of the for loop uses the same variable (thus inheriting the value from the previous iteration).

It will work if you initialize j = null; inside the body of the for loop because then you're reinitializing it for each iteration rather than using the value from the previous iteration.

ES6 proposes to add the let declaration which would scope to the nearest block. See What's the difference between using "let" and "var" to declare a variable? for more info on let.

Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • But this isn't inside a function. So is j global? – Yamcha Jan 02 '15 at 04:23
  • @user1316459 - I added a bit to my answer to include that case too. The point here is that the variable declaration is not block scoped so it's value persists from one iteration of the `for` loop to the next and that's what causes the behavior the OP sees. – jfriend00 Jan 02 '15 at 04:24
2

There isn't a block scope in JavaScript, only global and function scopes. Although, JavaScript variable statements are so flexible that they will give the programmer the illusion that a block scope could exist, but the truth is that variables declared in JavaScript functions are later hoisted by the interpreter. This means that their declarations are moved to the top of the most-recently declared function. Take this for example:

function test() {
   console.log('a test');
   for (var i = 0; i < 100; i++) {
      var k = i + Math.random();
      console.log(k)
   }
}

The JavaScript interpreter, internally, will "transform" the code to the following:

function test() {
   var i, k; // var declarations are now here!

   console.log('a test');
   for (i = 0; i < 100; i++) {
      k = i + Math.random();
      console.log(k)
   }
}

It will move all the var declarations to the begining of the most recent function declaration. In your code, the hoisting will create the following code:

// A anonymous function created by jsFiddle to run your script
function () {
   var i, j;

   for (i = 1; i < 3; i++) {
      if (!j) {
         j = 1;
      } else {
         alert("Why are we here? j shouldn't be defined, but it's " + j);
      }
   }
}

The variable is undefined in the first time, then it gets assigned 1 and then your message is printed.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
higuaro
  • 15,730
  • 4
  • 36
  • 43
  • 1
    The OP's issue is not caused by hoisting - it's caused by scoping rules. – jfriend00 Jan 02 '15 at 04:25
  • 1
    @jfriend00 Yes and no. If a var were re-declared as `undefined` in every loop, the issue you not be present. – Alexander O'Mara Jan 02 '15 at 04:27
  • 1
    @AlexanderO'Mara - I guess I can see your line of thinking, but the issue is that the variable is not redefined in each iteration of the loop and thus retains its value from one iteration of the loop to the next. Hoisting has to do with "where" the variable is first defined when the code runs. That isn't the exact issue here. The issue here is that the variable is only defined once within its scope and thus retains its value from one iteration of the `for` loop to the next. – jfriend00 Jan 02 '15 at 04:34
  • @jfriend00 Agreed. The hoisting example is more of a conceptual description for explaining how the code is interpreted. – Alexander O'Mara Jan 02 '15 at 04:35