The Task class destroys closures.

Tom Swirly's icon

Javascript closures are very handy, particularly as "private members" for a class. Unfortunately, the Task class seems to destroy a function's closure when it executes - even if it executes immediately!

Please see the code below. Note that there are two variables, foo and bar. foo is stored in the closure, bar is an instance variable.

Both foo and bar are available when the functions are called immediately, or if Javascript's apply function is used. But only the instance variable is available if an instance of Task is used to hold the variable - even if the result is not deferred in time but is immediately executed().

autowatch = 1;

function Closure() {
  var foo = 1;
  this.bar = 2;

  this.Foo = function() {
    post('Foo');
    post(foo, 'n');
  };

  this.Bar = function() {
    post('nBar');
    post(this.bar, 'n');
  };
};

var cl = new Closure();

post('*** immediate ***n');
cl.Foo();
cl.Bar();

post('*** apply (no this) ***n');
cl.Foo.apply();
cl.Bar.apply();  // This won't work.

post('*** apply (with this) ***n');
cl.Foo.apply(cl);
cl.Bar.apply(cl);  // Now it works.

// So far, so good:  this is exactly like the Javascript
// specification.  But the extension Task does not preserve
// the closure...

post('n*** Task.execute ***n');

var foo_task = new Task(cl.Foo, cl);
var bar_task = new Task(cl.Bar, cl);

foo_task.execute();
bar_task.execute();

post('n*** Task.schedule ***n');
foo_task.schedule(100);
bar_task.schedule(200);

/* Results:                                                    

  *** immediate ***
  Foo  1
  Bar  2                                                       

  *** apply (no this) ***
  Foo  1
  Bar                                               

  *** apply (with this) ***
  Foo  1
  Bar  2                                                       

  *** Task.execute ***
  Foo
  Bar  2                                                       

  *** task.schedule ***
  Foo
  test.  Javascript ReferenceError: foo is not defined, line 8
  Bar  2
*/

This is definitely a bug: closures should continue to work no matter where they're called from. It has nothing to do with the value of "this" - closures are independent of "this".

Tom Swirly's icon

I have a nice general workaround for this!

If you just wrap your object containing closures as a member variable in another object passed to Task, then all is fine.

I'll post the final code if there's any interest.

Tom Swirly's icon
// Tasker is a class with a single method, Run, that applies the given
// function with the given "this" and arguments.
//
// Tasker works around the issue documented in
// http://tinyurl.com/max-js-task-destroys-closures.
//
function Tasker(fn, thisArg, arguments) {
  this.thisArg = thisArg;
  this.fn = fn;
  this.arguments = arguments;

  this.Run = function() {
    this.fn.apply(this.thisArg, this.arguments);
  };
};

robertchudoba's icon

Yo.. has it always been like this in the Max JS implementation? Just asking because i just ran into the same problem and the thread is just one week old :|...

Tom Swirly's icon

Sorry I missed these updates!

The problem appears to persist in Max. My guess is that this is a bug where the context is accidentally garbage collected where it shouldn't be.

The code has now moved to http://github.com/rec/swirly/blob/master/js/swirly/util/tasker.js and should stay there for the foreseeable future.

Tom Swirly's icon

I have a demonstration of the problem and the fix, and I've simplified the code considerably. Please see http://ax.to/jstasks (which just points to the aforementioned http://github.com/rec/swirly/blob/master/js/swirly/util/tasker.js )

Tom Swirly's icon

ARG! Turns out I don't need that class at all.

Full code with documentation that demonstrates this issue and presents a workaround is still at http://ax.to/jstasks .

Peter Castine's icon

The GitHub link is now 404-ing. Was there anything beyond what is up at ?

Charles Turner's icon

Hi Peter-

Swirly's stuff is now at:

HTH, Moogie

Tom Swirly's icon

Oh, sorry about that!

I corrected the redirect - it now links to this code. But this is not a class you can re-use - it's a demonstration of a simpler way to fix the issue.

tasker.js is gone. That was my first try at fixing the issue - later I realized that you don't need a class at all.

It turns out that Task allows you to add more parameters at the end of its constructor - and those are passed to your callback function. (The documentation only mentions one argument, but you can actually pass as many as you like.)

So the takeaway is: Explicitly pass any values you need to use later to the constructor of Task, because your local context variables will all be empty later.