1

For your convenience, here's an interactive jsfiddle version of my code. Here's the offending code:

for i in [1, 2, 3, 4, 5, 6, 7, 8]
  console.log "cell #{i} was created!"
  cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
    alert "#{i} was clicked!"
  $("#inventory-grid").append(cell)

And here's the html:

<div id="inventory-dialog" class="dialog">    
    <div id="inventory-grid"></div>    
</div>

Here's how it's supposed to work. This will generate a bunch of cells in a loop. If I click on the first one I want it to alert, "1 was clicked!" and when I click on the last, I want it to say, "8 was clicked!" But for some reason, every one I click on says, "8 was clicked". Why is this happening?

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • 2
    You'll need to read into javascript closures and variable references, the value of i at the point of execution is 8, if you were able to click hypersonically fast you would get a value somewhere in between 1 - 8 depending on which iteration your outer loop creating the closure was at – Bryan Aug 16 '13 at 03:21
  • I think this is a good link to what @BryanMoyles is talking about. Unfortunately it's not specifically geared towards helping my issue but it's important to know the information: http://stackoverflow.com/questions/111102/how-do-javascript-closures-work – Daniel Kaplan Aug 16 '13 at 03:37
  • 1
    This is more specific to your issue: http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – bfavaretto Aug 16 '13 at 03:45
  • @bfavaretto thanks. I like Blender's answer better than that one though – Daniel Kaplan Aug 16 '13 at 03:53

5 Answers5

4

All of the callbacks refer to the same i variable in their body, which will have a value of 8 by the time any of those callbacks have been called.

You need to create a variable local to each specific callback which holds the value of i at the time the callback was created:

(function(j) {
    var cell = $('<div class="inventory-cell"></div>').mousedown(function(event) {
        alert("#{j} was clicked!");
    });

    $("#inventory-grid").append(cell);
})(i);

The functionally equivalent CoffeeScript would be:

do (i) ->
  cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
    alert "#{i} was clicked!"

  $("#inventory-grid").append(cell)

The only difference is that do will shadow i instead of creating a new variable, but the result is the same.

An exact translation would be:

do (j = i) ->
  cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
    alert "#{j} was clicked!"

  $("#inventory-grid").append(cell)
Blender
  • 289,723
  • 53
  • 439
  • 496
2
for i in [1, 2, 3, 4, 5, 6, 7, 8]
    do (i) ->
        console.log "cell #{i} was created!"
        cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
            alert "#{i} was clicked!"
        $("#inventory-grid").append(cell)

Fiddle.

pdoherty926
  • 9,895
  • 4
  • 37
  • 68
  • What's the `do`? I've never seen that before. Is that a JavaScript thing or a CoffeeScript thing? – Daniel Kaplan Aug 16 '13 at 03:30
  • 2
    "CoffeeScript provides the do keyword, which immediately invokes a passed function, forwarding any arguments." - http://coffeescript.org/ – pdoherty926 Aug 16 '13 at 03:31
1

You're already using jQuery, and since for-in loops shouldn't be used on arrays (cofeeScript sucks and apparenty renames native javascript stuff), you can just avoid the closure problems with $.each, like so:

$.each([1, 2, 3, 4, 5, 6, 7, 8], function(_,i) {
    var cell = $('<div />', {
        'class': 'inventory-cell',
         on    : {
                  click: function() { alert('#{'+i+'} was clicked!'); }
                 }
    });
    $("#inventory-grid").append(cell);
});

FIDDLE

adeneo
  • 312,895
  • 29
  • 395
  • 388
  • Upvote, but IMO my original code (though not working) looks cleaner than this. Why shouldn't for-in loops be used on arrays? – Daniel Kaplan Aug 16 '13 at 03:35
  • 1
    CoffeeScript `for...in` loops aren't the same as JS `for...in` loops. – Blender Aug 16 '13 at 03:35
  • You beat me to it, @Blender. "Most of the loops you'll write in CoffeeScript will be comprehensions over arrays, objects, and ranges. Comprehensions replace (and compile into) for loops, with optional guard clauses and the value of the current array index. Unlike for loops, array comprehensions are expressions, and can be returned and assigned." – pdoherty926 Aug 16 '13 at 03:36
  • @Blender - don't know much coffeeScript, but it still looks icky. – adeneo Aug 16 '13 at 03:36
  • @adeneo: `for...in` was renamed to `for...of` in CoffeeScript. `for...in` is translated to a regular `for` loop in the resulting JavaScript. – Blender Aug 16 '13 at 03:37
  • @adeneo: "*cofeeScript sucks and apparenty renames native javascript stuff*": JavaScript's "native stuff" is often clunky and stupid to begin with. I don't use CoffeeScript, but JavaScript isn't as developer-friendly as it should be. – Blender Aug 16 '13 at 03:41
  • 1
    @Blender - That's true, but renaming something as basic as `for..in` to `for..of` and doing something completely different for `for..in` is just confusing and in my opinion really, really stupid. They could have just named it something else instead – adeneo Aug 16 '13 at 03:44
  • @adeneo: It's a different language with a different syntax. `for...in` wasn't "renamed". It serves a different purpose in CoffeeScript. – Blender Aug 16 '13 at 03:46
  • 1
    @Blender - I know, but it still ends up as javascript, and it looks like javascript, and the selling point is "easier" javascript, and doing stupid naming stuff like that makes no sense to me when they could have used any other name but `for..in`. Really doesn't matter, I don't use CoffeeCrap, and I have no intention of starting. – adeneo Aug 16 '13 at 03:49
  • @adeneo: It looks nothing like JavaScript. The only similarity is the ridiculous number of callback functions. Imposing JavaScript's syntax onto a language that isn't JavaScript is stupid. – Blender Aug 16 '13 at 03:54
1

These two Coffeescript iterations produce the same Javascript function. They just iterate over it in different ways. One uses for (var i=0 ...), the other $.each()

for i in [1..8]
  do (i) ->
    console.log "cell #{i} was created!"
    cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
      alert "#{i} was clicked!"
    $("#inventory-grid").append(cell)
    return

$.each([1..8], (_,i)->
    console.log "cell #{i} was created!"
    cell = $('<div class="inventory-cell"></div>').mousedown (event) ->
      alert "#{i} was clicked!"
    $("#inventory-grid").append(cell)
    return
    )

Native Javascript array iteration could also be used:

[1..8].forEach (i)->

The other Javascript iteration method:

for (i in [1,2,3..]) {}

would have the same problems as the original Coffeescript if it didn't take care to localize the i used in the cell definition.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
0

Don't know about coffeescript... and either don't know why (function(){...})(); is error in fsfiddle..

for(var i=1; i<9;i++){
console.log("cell "+ i + "was created!")
~function(i){
    var cell = $('<div class="inventory-cell"></div>').mousedown(function(){
    alert(i+" was clicked!")
    });
  $("#inventory-grid").append(cell)
}(i);
}
Fly_pig
  • 155
  • 8