1

Possible Duplicate:
How do JavaScript closures work?

I've searched the FAQ and I've seen examples but I can't seem to grasp why this isn't working. I'd really appreciate any hints at all as to what I'm doing wrong. All I'm trying to do is take a word and on button click display images one at a time for each letter, spelling out the word (and images should fade in/out). It's the classic "for loop only shows last item" problem but the thing is, the console logs correctly. The variable changes, but only the last image displays. Again I truly appreciate any help understanding what I'm doing wrong as I know it's important to fully grasp this. Code below (I left out the HTML as it's just a div that gets updated and a button):

$(document).ready(function () {
  var word = 'abc';

  $('#newWordButton').click(function () {
    function animateLetters() {
      function changeLetter() {
        for (i = 0; i < word.length; i++) {
          var currentLetter = word.charAt(i);
          console.log(currentLetter);
          $('#wordsDiv').fadeOut(1000, function () {
            $('#wordsDiv').replaceWith('<img src = "images/letters/' + currentLetter + '.gif" />');
            $('#wordsDiv').fadeIn(1000);
          });
        }
      }
      setTimeout(changeLetter, 1000);
    }

    animateLetters();
  });
});
Community
  • 1
  • 1
  • At least indent your code properly so people can see the function scope more easily. Thanks to @Yoshi for fixing it. – Matt Burland Nov 19 '12 at 16:57
  • Why are you defining `animateLetters` inside of the click handler? It will be defined each time the element is clicked... – Shmiddty Nov 19 '12 at 17:00
  • bjornd -- I realize it's a possible duplicate and in fact stated that I'm aware it's been answered but that I don't understand the answer. – theFinalPastry Nov 19 '12 at 17:09

4 Answers4

0

You need to make currentLetter a closure by passing the value as an argument to a self-executing function. This will retain the value for you when your function executes.

Untested example:

for (i = 0; i < word.length; i++) {
    (function (currentLetter) {
        console.log(currentLetter);
        $('#wordsDiv').fadeOut(1000, function () {
            $('#wordsDiv').replaceWith('<img src = "images/letters/' + currentLetter + '.gif" />');
            $('#wordsDiv').fadeIn(1000);
        });
    )(word.charAt(i));
}
gpojd
  • 22,558
  • 8
  • 42
  • 71
  • 1
    That's doesn't answer his question, it might solve the problem, but it doesn't help their understanding. – ilivewithian Nov 19 '12 at 16:58
  • I am typing out the explanation, sorry. – gpojd Nov 19 '12 at 16:58
  • Thank you! I'm still having trouble understanding a bit (please forgive me, I'm a complete noob and none too bright to boot). So does the first function return currentLetter? – theFinalPastry Nov 19 '12 at 17:05
  • What I'm not grasping is the parentheses around the first function. I've not seen that notation and don't understand why it's needed/what it means. – theFinalPastry Nov 19 '12 at 17:12
  • @theFinalPastry, the parens around the function is to allow it to be a self-executing function. `(function (arg1Closure) {...})(arg1);` makes a function and executes it immediately with the arguments saved in that scope (i.e. a closure). Your lack of understanding is because I am not explaining it well. – gpojd Nov 19 '12 at 17:18
  • No, you just explained it fantastically! I finally get it! Thanks so much gpojd for your patience and kindness. I'm sorry I don't have points to give you. – theFinalPastry Nov 19 '12 at 17:28
0

The short answer is that closures are defined by the scope of a function, and not just any pair of angled brackets. Since a for loop isn't a function definition, it doesn't form a closure.

@gpojd provided a workable bit of code, I think. But there's more information on the closure FAQ, like:

Community
  • 1
  • 1
slashingweapon
  • 11,007
  • 4
  • 31
  • 50
0

The closure isn't the half of your problems...

The for loop prevented anything from actually being animated... You were replacing the #wordsDiv with an image, so it couldn't possibly exist for the next image.

See here: http://jsfiddle.net/P8Lz5/

$(document).ready(function() {
    var word = 'pneumonoultramicroscopicsilicovolcanoconiosis';
    $('#newWordButton').click(animateLetters);

    function animateLetters() {
        (function changeLetter(i) {
            if (i == word.length) return;

            var currentLetter = word.charAt(i);
            console.log(currentLetter);

            $('#wordsDiv').empty();
            var img = new Image();
            img.src = 'http://placehold.it/100x100/&text=' + currentLetter;

            img.onload = function() {
                $(img).appendTo('#wordsDiv').hide().fadeIn(1000, function() {
                    $('#wordsDiv img').delay(1000).fadeOut(1000, function() {
                        changeLetter(i + 1);
                    });
                });
            };
        })(0);
    }
});​

Or see here if you want the letters to fade in one at a time to form a word: http://jsfiddle.net/P8Lz5/1/

Shmiddty
  • 13,847
  • 1
  • 35
  • 52
0

JavaScript is function-scoped. Every time you create a new function, it:

  1. Gets a scope object which holds its own declared variables (including argument variables)
  2. Has a reference to the scope in which it was defined, which is walked at runtime when you attempt to access a variable that isn't present in the function's own scope.

So, looking at a slightly edited version of your code (added var to the loop so i isn't global, removed the animateLetters() wrapper layer):

$(document).ready(function () {
  var word = 'abc';

  $('#newWordButton').click(function () {
    function changeLetter() {
      for (var i = 0; i < word.length; i++) {
        var currentLetter = word.charAt(i);
        $('#wordsDiv').fadeOut(1000, function () {
          $('#wordsDiv').replaceWith('<img src = "images/letters/' + currentLetter + '.gif" />');
          $('#wordsDiv').fadeIn(1000);
        });
      }
    }
    setTimeout(changeLetter, 1000);
  });
});

This is what the resulting scopes which are generated look like (with a scope represented by square brackets):

[global] - this is the fallback scope, and how every reference to $ is resolved
| var $
+-[function()] (passed to ready call)
  | var word
  +-[function()] (passed to click call)
    | var changeLetter
    +-[function changeLetter()]
      | var i = 4
      | var currentLetter = 'c'
      +-[function()] (passed to 1st fadeOut call)
      | |
      +-[function()] (passed to 2nd fadeOut call)
      | |
      +-[function()] (passed to 3rd fadeOut call)
        |

When your fadeOut functions are called, they walk up the scope chain to find the currentLetter variable when it's accessed, as they don't have one in their own scope. Notice that they all have access to the same parent scope, so they're all accessing the same currentLetter variable which, after the for loop has exited, will be pointing at 'c'.


This is what the bottom of the scope diagram looks like when you use @gpojd's solution (but don't forget to add var i to the loop expression!), after the for loop has exited:

[function changeLetter()]
| var i = 4
+-[function()] (IIFE in 1st loop iteration)
| | var currentLetter = 'a'
| +-[function()] (passed to 1st fadeOut call)
|   |
+-[function()] (IIFE in 2nd loop iteration)
| | var currentLetter = 'b'
| +-[function()] (passed to 2nd fadeOut call)
|   |
+-[function()] (IIFE in 3rd loop iteration)
  | var currentLetter = 'c'
  +-[function()] (passed to 3rd fadeOut call)
    |

The solution is using an Immediately-Invoked Function Expression (IIFE) to create an intermediate function scope on each loop iteration, which passes the value of the character at the current value of i into what becomes a currentLetter variable in its own scope - now when your fadeOut functions are called, they find and use the currentLetter variable in the function scope created by the IIFE.

Jonny Buchanan
  • 61,926
  • 17
  • 143
  • 150
  • Wow, thanks so much! I will be spending a good deal of time with your thorough example. I was about to give up and thanks to you and the others I'm learning something. Truly appreciated! – theFinalPastry Nov 19 '12 at 17:50
  • You're welcome :) Recommended reading: http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ – Jonny Buchanan Nov 19 '12 at 20:07