Search code examples
javascriptcallbacknode-cron

JavaScript Bind (this) without access to class


I am having trouble accessing a class' methods without some modifications to the class itself.

See the following demo code:

function myCallback() {
  this.otherMethod();
}

class hiddenClass {

  static hiddenFunction(callback) {
    callback();
  }
  
  static otherMethod(){
    console.log("success");
  }
}

hiddenClass.hiddenFunction(myCallback);

In this simple example, I want to access this.otherMethod() in my callback. The obvious answer is to change callback() to callback.bind(this)(), however in this case, hiddenClass would be a library.

How can I call this other method?

For reference, I am trying to create cron jobs using node-cron. I want to destroy these jobs if a database check returns true, checking every cycle of the job (in the callback). The job has an internal method .destroy(). I am aware I can save the cron job in a variable, then call variable.destroy(), but is there a way of doing it in the callback (as these jobs are created in a for loop and I don't want to pinpoint which one to destroy from inside the callback)

cron.schedule(`5 * * * * *`, async function () {
  schedule = await Schedule.findById(schedule._id);
  if (!schedule) this.destroy() // Destroy the job if schedule is not found (this returns Window)
}

);


Solution

  • Your reasoning is sound, you would need to bind the callback to the class in question. However, it could be done in your sample code by modifying the call to hiddenFunction:

    hiddenClass.hiddenFunction(myCallback.bind(hiddenClass));
    

    This won't require a modification to the library.


    As for your reference code...

    The pre-ES2015 way of doing this would be to create anonymous functions bound by calling with the current loop iteration's value, for a simple example:

    var schedules = [1,2,3,4,5];
    for(var i=0;i<schedules.length;i++){
      setTimeout(function(){console.log(schedules[i])}, 10);
    }
    /* => outputs:
    5
    5
    5
    5
    5
    */
    for(var i=0;i<schedules.length;i++){
      // create an anonymous function with the first arg bound at call time
      (function(j){
        setTimeout(function(){console.log(schedules[j])}, 10);
      }(i));
    }
    /* => outputs:
    1
    2
    3
    4
    5
    */
    

    HOWEVER, this would not be possible in your code as you're trying to pass an anonymous callback and reference the current loop's iteration value in said function, requiring the anonymous function to know the return value of cron.schedule before itself being passed to cron.schedule.

    The solution to the problem is to use let or const in your loop to bind the current iteration's value. They are block scoped, not lexically scoped, so they retain the value at each iteration resulting in N closures of the same anonymous function. We can demonstrate the difference between the two in the following loop:

    for(var foo of [1,2,3,4,5]){
      const bar = foo;
      (async function(){
        await Promise.resolve().then(()=>console.log({bar, foo}));
      }());
    }
    
    /* outputs:
    { bar: 1, foo: 5 }
    { bar: 2, foo: 5 }
    { bar: 3, foo: 5 }
    { bar: 4, foo: 5 }
    { bar: 5, foo: 5 }
    */
    

    Tying it all together, your loop scheduler would look like the following:

    for(let schedule of schedules){
      const me = cron.schedule(`5 * * * * *`, async function () {
        // the value of `me` is bound to each iteration and you can refer to it as needed
        schedule = await Schedule.findById(schedule._id);
        if (!schedule) me.destroy();
      });
    }