Search code examples
node.jsbluebird

Iteratee function loses reference to this object in javascript if used with bluebird map function


I am using bluebird npm package for handling asynchronous workloads in my application. Lets assume that processAllItems function is used to trigger processing of all the items. Each one of the items from the list, needs to be processed using function processOneItem which processes them one by one. It internally calls few functions from the same class. Bluebird library's .map function is used to manage concurrency of execution here. So, my code looks something like this.

const Bluebird = require('bluebird');

module.exports = function (app) {
  return {

    doSomething: function () {
      return new Promise(function (resolve, reject) {
        // do something and resolve
      });
    },

    processOneItem: function (item) {
      let self = this;
      return new Promise(function (resolve, reject) {
        // blah
        self.doSomething()  //self is undefined
          .then(function () {
            //do something else and so on...
          })
      });
    },

    processAllItems: function () {
      const self = this;
      return new Promise(function (resolve, reject) {
        // processOneItem function is to be called for an array of data
        // lets assume
        let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        Bluebird.map(array, self.processOneItem, { concurrency: 5 })
          .then(function () {
            return resolve();
          })
          .catch(function (error) {
            return reject(error);
          });

      });
    }
  };
};

Since functions doSomething, processOneItem and processAllItems are keys of the same object, they need to be accessed by the this object, which is assigned to self variable. This all makes sense from javascript perspective so far. But when I execute this code, I get error saying Cannot read property 'doSomething' on line self.doSomething() inside function processOneItem. How is it possible that the reference to this object is lost? Am I doing something wrong here?

One thing I noticed, if I wrap the iteratee function inside another unnamed function, this seems to work perfectly fine.

const Bluebird = require('bluebird');

module.exports = function (app) {
  return {

    doSomething: function () {
      return new Promise(function (resolve, reject) {
        // do something and resolve
      });
    },

    processOneItem: function (item) {
      let self = this;
      return new Promise(function (resolve, reject) {
        // blah
        self.doSomething()  //self reference is maintained, works perfectly fine.
          .then(function () {
            //do something else and so on...
          })
      });
    },

    processAllItems: function () {
      const self = this;
      return new Promise(function (resolve, reject) {
        // processOneItem function is to be called for an array of data
        // lets assume
        let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        Bluebird.map(array, function (record) { return self.processOneItem(record); }, { concurrency: 5 })
          .then(function () {
            return resolve();
          })
          .catch(function (error) {
            return reject(error);
          });

      });
    }
  };
};

How is it possible that when the line

Bluebird.map(array, self.processOneItem, { concurrency: 5 })

is replaced with line

Bluebird.map(array, function (record) { return self.processOneItem(record); }, { concurrency: 5 })

the reference to this object is maintained inside the iteratee function and code works perfectly fine.


Solution

  • This is a question of javascript binding. Here is a simplification of your case above that yield the same result regardless of bluebird.

    const obj = {
        a: function(){
            console.log('A THIS!', this)
        },
        
        b: function(){
            console.log('B THIS!', this)
        }
    
    }
    
    function c_function(paramsFunction){
        paramsFunction();
    }
    
    
    obj.a() // A THIS!, {a: function(), b: function()}
    obj.b() // b THIS!, {a: function(), b: function()}
    
    c_function(obj.a) //"A THIS!" global {Buffer: function(), clearImmediate: function(), clearInterval: function(), clearTimeout: function(), …}
    
    
    c_function(function() { obj.a()}) // A THIS!, {a: function(), b: function()}
    

    The this of the javascript for a function execution is binded at the execution of that function.

    1. List item

    So when calling

    obj.a()
    

    It check for the this rules and find it was called on an object so the this will be obj.

    1. When calling
    c_function(obj.a)
    

    c_function receive a function as an argument. When it execute the function it try to bind the this. As c_function scope is concerned, the function is not called on an object. Therefore it will bind the this according to the rules. Here c_function is binded to the global this and executing paramsFunction() the binding will be the same.

    1. When calling
    c_function(function() { obj.a()}) // Or
    c_function(() => { obj.a()})
    

    All the knowledge of the 2 previous are combine. The important takeaway is that at the moment of the call you are calling from obj

    EXTRA Note that the map function may call your function with a bind, call or apply. This will affect the scope of the executed function.

    function c_function_binding(paramsFunction) {
        paramsFunction.call({}, paramsFunction)
    }
    
    c_function_binding(obj.a) // "A THIS!" {}
    

    I was not able to find it, but a few years ago Kyle Simpson had a course or a workshop that explained in detail the scope binding in javascript and all its quirks.