Monday, February 4, 2013

JavaScript - Scalar Promotion aka Hoisting

Scalar Promotion or Hoisting is a property of JavaScript that is often ignored but can cause some pretty crazy issues. (I like the term Scalar Promotion because it helps me fool people into thinking I'm smart and "Code Motion" sounds like I'm bullshitting too hard. Haha). To illustrate what it is and how it works, I'm going to show a few code examples. Take the following, very simple code for example:

(function() {
    var foo = 'abc',
          bar = 'xyz';
    console.log(foo + bar); //abcxyz
})();

The output of the above, as you might expect, will be abcxyz. Now lets have a look at what happens if we don't assign bar:


(function() {
    var foo = 'abc',
          bar;
    console.log(foo + bar); //abcundefined
})();


Now the output is going to be abcundefined. So what happens if we remove the declaration of bar?


(function() {
    var foo = 'abc';
    console.log(foo + bar); //ReferenceError!!!
})();


Now we get a ReferenceError, because bar was never declared. Well that makes sense, right? But to show you what Scalar Promotion is all about have a look at this.. we'll move the declaration of bar to somewhere below the console.log():

(function() {
    var foo = 'abc';
    console.log(foo + bar); //abcundefined again
    var bar;
})();

... and the result is abcundefined. Okay, what the tell right? Why does this behave that way? Scalar Promotion. Basically as a compiler optimization, all declarations are moved to the top of your code, so to speak. This is why you can use a function in code before you've declared it in javascript:

(function (){
  callMe();

  function callMe() {
    alert("I've been called!");
  }
})();


The reason this can become problematic is that JavaScript doesn't force you to declare your variables. So consider the following code:

// A Tale Of Two Foos
(function (){
    
    // an anonymous function to do something or other asynchronously
    // this could be anything, like an ajax callback for example.
    setTimeout(function (){
        
        //manipulate foo, but forget to declare it.
        for(var i = 0; i < 10; i++) {
          foo += 'x';   
        }
        
        // SORCERY?!!! 45xxxxxxxxxx?! 
        // this should just be ten  x's!
        console.log(foo);
    },0);
    
    /* ---
    assume some large amount of code here. So you don't 
    know/remember these two foos are in the same scope.
    --- */
    
    // declare foo and add some numbers to it.
    var foo = 0;
    for(var i = 0; i < 10; i++) {
        foo += i;
    };
    console.log(foo); //45
})();


Do you see what happened there? One little missed declaration and there's no error... just a bug. Generally the only way to avoid this is to try to declare your variables at the beginning of your function scope if possible/practical. Either way, be careful to declare you variables, kids!

5 comments:

  1. How would this happen without the setTimeout function?

    ReplyDelete
  2. The setTimeout function was added there to delay the call and add an interior closure to the code. If you where to simply comment out the setTimeout and just have an interior function and call it at the very end, you'd see the same result. If you were to just comment the setTimeout stuff out and leave the interior code where it is, you'd still see the 45xxxxxxxxxx logged, but it would be logged at the second console.log reference. If that makes sense.

    ReplyDelete
  3. With the setTimeout function this seems to behave exactly as it is supposed to, since the code passed to setTimeout doesn't even execute until the context in which it is running has finished. Similarly, if I put the function at the bottom it would be delivering the expected result, since the for loop would be coming before it.

    Now, if I comment out the setTimeout function and leave the interior code where it is, it seems that it doesn't get the 45. http://jsfiddle.net/Rvsnh/

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. I messed up my link in that last comment. I've forked your fiddle and annotated it to explain what's happening. http://jsfiddle.net/blesh/YBE5E/2/

    ReplyDelete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)