Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript closure behaves differently when bound to an event

I am trying to use a closure to ensure that a function can only execute once. Sounds simple, and it works like this:

function runOnce(fn)  // returns copy of fn which can only execute once
{
    var ran = false;

    return function() 
    {
        if (!ran)
        {
            fn();
            ran = true;
        }
    };
}

I have tested the function like so:

function lazyLoadGrid(event, ui)
{
    alert('hi');
}

var test1 = runOnce(lazyLoadGrid);
var test2 = runOnce(lazyLoadGrid);

test1();
test2();

test1();
test2();

And it works as expected - 'hi' gets alerted exactly twice.

But then I try to use runOnce(lazyLoadGrid) as the callback to a jQuery UI event:

$('.accordion').each(function() 
{ 
    $(this).accordion({ autoHeight: false, change: runOnce(lazyLoadGrid) });
});

And madness ensues. What I expect is that each 'accordion' on the page will run lazyLoadGrid() exactly once, when that accordion is first opened. Instead, the closure callbacks seem to behave as if they are all referencing the same copy of 'ran'. lazyLoadGrid() runs the first time I open any accordion, and then never runs again for any other accordion. Logging the pre-condition value of 'ran' shows that it's 'true' every time I click any accordion after the first one.

What is the explanation for this? It may be worth noting I have an odd page, with nested accordions, and multiple jQuery UI tabs each containing accordions. To make matters worse, when I switch tabs the closure actually does run on the first-opened accordion of any given tab. Any advice is much appreciated.

like image 717
sacheie Avatar asked Feb 29 '12 23:02

sacheie


People also ask

How does closures work in JavaScript?

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.

What is closure in JavaScript with real time example?

In JavaScript, closures are defined as inner functions that have access to variables and parameters of outer function even after the outer function has returned.

Does setTimeout create closures?

It's important to understand that closures are created when functions are created, not when they are invoked. And because a closure was created when this setTimeout function was created, this enables the setTimeout function to access the i variable at whatever time the setTimeout function will run.

What would happen if I remove the closure feature from JavaScript?

If JavaScript did not have closures, then more states would have to be passed between functions explicitly, making parameter lists longer and code noisier. So, if you want a function to always have access to a private piece of state, you can use a closure.


2 Answers

The problem:

I believe the trouble you are having is because what you are thinking of as an "accordion" is actually a "panel". The accordion consists of all the panels in a group. It sounds like you want to run it once per panel, not once per accordion. The following demo illustrates the concept by including two accordions on a page. Notice that lazyLoadGrid() is run twice, once for each accordion:

http://jsfiddle.net/cTz4F/

The solution:

Instead what you want to do is create a custom event and call that event on each panel. Then you can take advantage of jQuery's built-in .one() method which causes that an event handler is called exactly once for each element:

$('.accordion').accordion({
    autoHeight: false,
    change: function(e, ui) {
        ui.newHeader.trigger("activated");
    }
});
$('.accordion > h3').one("activated", lazyLoadGrid);

Working demo: http://jsfiddle.net/cTz4F/1/

like image 155
gilly3 Avatar answered Oct 25 '22 19:10

gilly3


How about:

function runOnce(fn) {
    return function(){
      fn();
      fn = function(){};
  }
}

// test 
var foo = function(){
    console.log('bar');
}
foo = runOnce(foo);
foo(); // bar
foo();
foo();
like image 22
u.k Avatar answered Oct 25 '22 18:10

u.k