2

I'm working with JQuery and jsTree, and I've run into some confusion about how closures work.

I have an object that has a .jsTree member, and a .populateTree method. The method is called with an array of strings, with which it is supposed to create the nodes of the jsTree.

The jsTree builds a tree control, in which every node has an anchor "", which contains the text of the node. I want to make clicking on the text toggle the node open or closed, just like clicking on the +/- button in the tree. So I'm trying to add a click() function to do that, and I'm getting unexpected behavior.

So, here is the code:

populateTree: function populateTree(nodeNames)
{
    if (!this.jsTree) // Note 1
        return;

    var me = this; // Note 2

    for (var i = 0; i < nodeNames.length; i++)
    {
        var nodeName = nodeNames[i];

        var node = this.jsTree.create_node(-1, "last", { state: 'open', data: nodeName }); //Note 3

        this.jsTree.create_node(node, "last", { data: "child one" }); // Note 4
        this.jsTree.create_node(node, "last", { data: "child two" });
        this.jsTree.create_node(node, "last", { data: "child three" });

        var anchor = node.find("a"); // Note 5
        anchor.click(function() { me.jsTree.toggle_node(node); }); // Note 6
    }
},
  • Note 1: This is a member function of a javascript object, so when it is called, "this" points to the object. The object contains a jsTree member variable, which should have already been initialized to contain a jsTree object, with no nodes.

  • Note 2: We are defining a "click" function at Note 6, and when that is called, "this" will not point to the object that contains the jsTree, so we save "this" in a variable called "me", which will be in scope when the "click" function executes, because creating the function created a closure, which included references to all variables that were in scope at the time the function was defined.

  • Note 3: For each element in the array, we create a top-level node (parent node is -1).

  • Note 4: For each top-level node we've created, we create three child nodes.

  • Note 5: Each node contains an anchor element (""), and it's this that we want to attach a "click" function to.

  • Note 6: Inside the "click" function, "me" should point to the object that contains the tree (see Note 2), and "node" should point to the node that we just created, in the current pass through the loop (see Note 3).

My problem? No matter which of the anchors I click on, it's always the last top-level node that opens and closes. It's like the closure for each of the "click" functions we've created has a closure that references only the last "node" variable. And that's not the way I thought closures worked.

Can someone help me understand where I went wrong in my understanding?

Thanks.

Vivin Paliath
  • 94,126
  • 40
  • 223
  • 295
Jeff Dege
  • 11,190
  • 22
  • 96
  • 165
  • 1
    An added point of clarification: JavaScript statement blocks (the curly-brace block of your "for" loop) do **not** create their own lexical scope. It's different from languages like C++ or Java. Only functions create new scope. – Pointy Mar 31 '11 at 16:06
  • you're having exactly the same problem as mentioned in [this question](http://stackoverflow.com/questions/341723/event-handlers-inside-a-javascript-loop-need-a-closure). refer to the accepted answer there for a solution – schellmax Mar 31 '11 at 16:04

3 Answers3

3

The anonymous function which you attach as your click handler closes over a single node instance, once the loops finished executing, and many seconds later when the user clicks on the tree that anonymous function is executed, it will look at the scope it closed over when created, and see that node's value is that which it last held, like you noticed, which is that of the final iteration of the loop.

A quick fix could be:

anchor.click((function(node){ return function() { me.jsTree.toggle_node(node); }; })(node));

This way, the closed over value of node is the passed value, which will hold a different value for each iteration.

davin
  • 44,863
  • 9
  • 78
  • 78
2

The issue is that in Javascript, blocks do not define scope, functions do.

So even though nodeName and node are defined inside the for loop, they act identical to if they were defined outside, because the for loop block does not create a new scope.

That's why in the book "Javascript: The Good Parts" Crawford recommends that in Javascript you define local variables at the beginning of a function rather than closest to it's usage like you would in other languages. Defining them further down makes it appear as if it were scoped inside the containing block when in reality they are not.

Davy8
  • 30,868
  • 25
  • 115
  • 173
1

Remember that closures are static. The value of the node used in your click handler will be that last value it was assigned, NOT the value it had when you the click handler was created.

The fix is to create a new closure for each click handler:

var anchor = node.find("a"); // Note 5
(function (node) {
  anchor.click(function() { me.jsTree.toggle_node(node); }); // Note 6
}) (node);

The anonymous function is called with the current value of node, and that is the one referenced when the click handler is invoked. Each click handler is provided with its own value of node

HBP
  • 15,685
  • 6
  • 28
  • 34
  • I think you're closing in on the problem I'm having. It's not that I don't understand closures, it's that I don't properly understand variables. In a sane language, there would be three different instances of "node", one in each execution of the loop, just as there are three different "click" functions. But, apparently, there aren't. – Jeff Dege Mar 31 '11 at 16:05