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: Insid开发者_开发问答e 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.
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.
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.
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
精彩评论