0

I have a directory with files that I need to parse and save in an array of JavaScript objects. In the constructor, as soon as I reach point 2 in the code below, this becomes undefined and in point 3 I don't have access to this.data. What's the best way to fix the code and achieve the goal?

function Service(path) {
    this.data = [];
    console.log(this); // point 1
    fs.readdir(path, function (err, files) {
        console.log(this); // point 2
        files.forEach(function (file) {
            fs.readFile(path + file, function (err, data) {
                parseString(data, function (err, result) {
                    this.data.push(result); // point 3
                });
            });
        }, this);
    });
}

Service.prototype.getData = function () {
   // do something with `this.data` and return the result
};

I tried saving this as self, but in the end self ends up with proper array, while this does not:

  function Service(path) {
    var self = this;
    this.data = [];
    fs.readdir(path, function (err, files) {
        files.forEach(function (file) {
            fs.readFile(path + file, function (err, data) {
                parseString(data, function (err, result) {
                    self.data.push(result);
                });
            });
        });
    });
    setTimeout(function () {
        console.log(this); \\ point x
        console.log(self); \\ point y
    }, 3000);    
  }

  Service.prototype.getData = function () {
    // do something with `this.data` and return the result
  };
krl
  • 5,087
  • 4
  • 36
  • 53
  • possible duplicate of [How to access the correct \`this\` / context inside a callback?](http://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-context-inside-a-callback) – Scimonster Dec 22 '14 at 20:11
  • 2
    Keep in mind, this example isn't very safe. You have an asynchronous constructor, but no callback or loaded method to use which will let you know when the data is ready. You can't just use: var service = new Service(path); service.getData();`. – Timothy Strimple Dec 22 '14 at 21:32
  • Yes, so what would be the best way to do it? Promises? – krl Dec 22 '14 at 21:46

4 Answers4

3

The this object changes anytime you change from a function's scope to another.

You can solve this by saving the this object you want to a variable (in my example: self):

function Service(path) {
    var self = this;
    self.data = [];
    console.log(this); // point 1
    fs.readdir(path, function (err, files) {
        console.log(self); // point 2
        files.forEach(function (file) {
            fs.readFile(path + file, function (err, data) {
                parseString(data, function (err, result) {
                    self.data.push(result); // point 3
                });
            });
        });
    });
}

Service.prototype.getData = function () {
    var self = this;

    setTimeout(function () {
        console.log(self.data);
    }, 100);
};

var serv = new Service();
serv.getData(); // your array

See documentation on Mozilla for this.

To see an example, please refer to this fiddle.

lante
  • 7,192
  • 4
  • 37
  • 57
  • Please see my 2nd example, using `self` instead of `this`. In that example `self` ends up with proper array but `this` does not. – krl Dec 22 '14 at 20:17
  • use `this` to assign it to another variable. After that, dont use it anymore. As I said, `this` may change anytime your scope change. – lante Dec 22 '14 at 20:18
  • In this case, How do I refer do `data` from `Service.prototype.getData = function() {...}` function? – krl Dec 22 '14 at 20:26
  • Use it like in the example above. Then you can call `new Service(path).data` and it should work fine. – Makaze Dec 22 '14 at 20:26
  • Put the entire prototype code in the OP if your issue includes those prototypes. – Makaze Dec 22 '14 at 20:26
  • @Makaze I need to access it from inside the object. See my comment above. – krl Dec 22 '14 at 20:26
  • `var self = this;`, then use `self` instead of `this` anywhere inside your function, even if it comes from a prototype assignment. – lante Dec 22 '14 at 20:28
  • @lante But how do I access `data` from another function of the same object in this case? `self` is not available in function prototypes of the object. – krl Dec 22 '14 at 20:34
  • you just assigned `data` to `this`, so just do `this.data`, (or `self.data`, after assigning `this` to `self`) from the other function. – lante Dec 22 '14 at 20:35
  • @lante Please see my code again In `point y`, I have proper array in `self`, but in `point x` `this` still has an empty array. So yes, I can access `this.data` from a function prototype, BUT it doesn't has empty array, not the actual data. I don't see why. – krl Dec 22 '14 at 20:40
  • I think it's because this.data has to be redefined. Posting working example as answer. – Makaze Dec 22 '14 at 20:44
  • 1
    I have updated my answer, please see [this fiddle](http://jsfiddle.net/Lgv18opp/). It may help. – lante Dec 22 '14 at 20:46
3

As an alternative to Vsevolod's answer, you can also use bind.

function Service(path) {
    this.data = [];
    console.log(this); // point 1
    fs.readdir(path, function (err, files) {
        console.log(this); // point 2
        files.forEach(function (file) {
            fs.readFile(path + file, function (err, data) {
                parseString(data, function (err, result) {
                    this.data.push(result); // point 3
                }.bind(this));
            });
        }, this);
    }.bind(this));
}

Bind is slower than using a closure, but that only really matters in very tight loops where you're doing a ton of iterations and is unlikely to be an issue in most scenarios.

Timothy Strimple
  • 22,920
  • 6
  • 69
  • 76
1

this refers to the 'current' context.

In you example you can just save the 'outer' this to a local variable

function Service(path) {
    var $this = this;
    this.data = [];
    console.log(this); // point 1
    fs.readdir(path, function (err, files) {
        console.log($this); // point 2
        files.forEach(function (file) {
            fs.readFile(path + file, function (err, data) {
                parseString(data, function (err, result) {
                    $this.data.push(result); // point 3
                });
            });
        }, $this);
    });
}

PS while using bind is not prohibited either, the solution with assigning this to a local variable looks more elegant and will ensure that you will not forget to add another bind to the callback

Vsevolod Goloviznin
  • 12,074
  • 1
  • 49
  • 50
1

I'm not sure why this is the case, but if you do this.data = this.data or self = this in your prototype it outputs the correct value. Otherwise it returns the whole Service instance at the time it was created. Here is a snippet you can try with a dummy Service and getData example.

function Service(array) {
  var self = this;
  this.data = [];
  console.log(this); // point 1
  array.forEach(function(item) {
    self.data.push(item); // point 2
  });
}

Service.prototype.getData = function() {
  self = this;
  return self.data;
};

var serv = new Service(['bob', 'aerith']);

console.log(serv.getData());
Makaze
  • 1,076
  • 7
  • 13