Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Closure Scope not captured? — Coffeescript

Okay, I don't know how to phrase the title for this question.

openDir = (path) ->
socket.emit "get_metadata", path, (data) ->
    columnBox = $ "<div/>", class: "columnbox"
    for item in data.contents
        itemBox = $ "<div/>", class: "itembox"
        itemBox.click ->
            columnBox_inner.children().removeClass "selected"
            itemBox.addClass "selected" # <<<--- Over here
            openDir item.path
        columnBox.append itemBox
    columnBox.appendTo "#columnscontainer"

I understand that the variable itemBox is defined under openDir's scope here. But since the pointed out line is in a lambda function, shouldn't itemBox there capture the object referenced by itemBox of the parent scope instead of getting mutated to the last object referenced by it?

To put it clearly, I expect the click handler of each itemBox to perform addClass "selected" to itself. But what happens is that itemBox in each of the click handlers always refer to the last itemBox.

I can easily fix this by changing where itemBox gets declared. i.e. changing

for item in data.contents

into

data.contents.forEach (item) ->

But I'd like to know why the lambda function does not capture the variables current value.

like image 920
Gautham Badhrinathan Avatar asked Aug 16 '12 22:08

Gautham Badhrinathan


1 Answers

This loop:

for item in data.contents
    itemBox = $ "<div/>", class: "itembox"

is somewhat deceptive if you're not used to (Coffee|Java)Script scope. The scoping actually looks more like this:

itemBox = undefined
for item in data.contents
    itemBox = $ "<div/>", class: "itembox"

so there is only one itemBox variable and that same variable gets used by each iteration of the loop. The click handler keeps a reference to itemBox but doesn't evaluate the variable until the click handler is called so all the handlers end up with the same itemBox value and that will be the itemBox value at the end of the loop.

From the fine manual:

When using a JavaScript loop to generate functions, it's common to insert a closure wrapper in order to ensure that loop variables are closed over, and all the generated functions don't just share the final values. CoffeeScript provides the do keyword, which immediately invokes a passed function, forwarding any arguments.

So you could do this:

for item in data.contents
    do (item) ->
        # As before...

to get your itemBox scoped to each iteration of the loop individually.

Using forEach:

data.contents.forEach (item) ->

instead of a simple loop works because you're effectively using a function as the loop's body and any variables inside that function will be scoped to that function.

like image 74
mu is too short Avatar answered Oct 14 '22 09:10

mu is too short