11

I'm using an ES6 class to bundle some functionality together in Node. Here's (basically) what it looks like:

class processDocs {
  constructor(id) {
    this.id = id;
    // console.log(this) returns { id: id }
  }

  getDocs(cb) {
    // console.log(this) returns null
    docs
      .query(qb => {
         qb.where('id', this.id);
      })
      .fetch()
      .then(function(documents) {
        cb(null, documents);
      })
    ;
  }

  alterDocs(documents, cb) {
    //some logic
  }

  reindexSearch(cb) {
    //some logic
  }

  process() {
    // console.log(this) returns { id: id }
    async.waterfall([
      this.getDocs,
      this.alterDocs,
      this.reindexSearch
    ]);
  }
}


export default processDocs;

I thought that with ES6 classes, the way to assign public variables was to simply reference this and the way to initialize those variables via a constructor is exactly how it shows up in my class definition.

Here's how I'm calling the class (in a separate file):

var Processor = require('./processDocs');

var pr = new Processor(id);
var docs;
pr.process();

Here's the issue, when I console.log out this from the constructor, I get my { id: id } value as predicted; however, whenever I log out this in getDocs when process is running, it's null. BUT, when I log out this in process() right before the waterfall, I get my original object.

Is there any reason for this?

Btw, I'm using node: v0.10.33 and babel-node 4.6.6 and I run babel-node with the --harmony flag. Before anyone asks, I can't update to a newer Node version due to a major dependency which is stuck at v0.10.x.

EDIT I was able to create a workaround but it's not very es6-like. The issue seems to be with async.waterfall. I had to use a .bind to fix it:

    async.waterfall([
      this.getDocs.bind(this),
      this.alterDocs.bind(this),
      this.reindexSearch.bind(this)
    ]);
Hugo Dozois
  • 8,147
  • 12
  • 54
  • 58
antjanus
  • 987
  • 3
  • 15
  • 30
  • I don't get what you mean by "not very es6-like"? Methods were not, are not, and will not be bound to the instance by themselves. Btw, if you want real ES6 code, then scrap `async` and use promises. – Bergi Mar 31 '15 at 00:46
  • I'm saying not very `es6-like` because I had to use `.bind(this)` when I should not have to. So are you saying that if I invoke a method from within a class, `this` will be lost? Because that doesn't make sense to me. When I used `pr.process()`, `this` was correct and when I invoked `this.getDocs` directly from `this.process()`, it retained its `this` as well. This seems to be an `async` issue. Also, If I wanted to be very ES6-like, I'd use generators :) Promises are ES5 and would still result in callback hell which is what I'm trying to prevent by using `async`. – antjanus Mar 31 '15 at 00:52
  • Why do you think you didn't need to use `.bind()`? Your passing references to function to somewhere else. You *are not invoking them* from within your class! Promises are the proper solution here (they're very much ES6! Generators are not an async feature!): `process() { return this.getDocs().then(docs => this.alterDocs(docs)).then(alteredDocs => his.redindexSearch(alteredDocs)); }` – Bergi Mar 31 '15 at 00:56
  • I must have misunderstood how ES6 classes work because it'd make more sense to me for a class method to keep a reference to the class rather than to a new context (which I'd expect with a free-standing function). However point taken (and I guess it does make sense in some way), I like your elegant promise solution so I'll go ahead and rewrite with that instead. Thanks! – antjanus Mar 31 '15 at 02:40
  • On second thought, one of those steps also uses async.each, any idea on how to handle that? – antjanus Mar 31 '15 at 02:43
  • A class method does keep its reference to the class (and `super` still works), but as it is still on the prototype, it cannot keep a reference to the instance it was accessed through. Loops with promises would be done [like this](http://stackoverflow.com/a/27500098/1048572) (though probably with arrow functions) – Bergi Mar 31 '15 at 02:48

4 Answers4

6

The ES6 did NOT change how the "this" works, therefore it depends on the context of "where you call it" rather than "always have same" value. It is quite unintuitive and it is not common in other languages.

Consider this example

class processDocs {
  constructor(id) {
    this.id = id;
    console.log(this)
  }

  getDocs(cb) {
    console.log(this)
  }

  alterDocs(documents, cb) {
    //some logic
  }

  reindexSearch(cb) {
    //some logic
  }

  process() {
    console.log(this)
  }
}

var process = new processDocs(10);
var docs = process.getDocs(function(){});
var processInstance = process.process();

var docsAsFunction = process.getDocs;
docsAsFunction(function(){});

The output is

processDocs {id: 10}
processDocs {id: 10}
processDocs {id: 10}
undefined

As you can see, the last one is undefines, which is calling "docsAsFunction", because you did not call that function directly from its class, therefore context is different.

You can read about it for example here

libik
  • 22,239
  • 9
  • 44
  • 87
  • took me months to get what you're saying but now I got it. Thanks for the answer! – antjanus Oct 05 '16 at 21:15
  • Wow, I always knew this was a problem in React but I just stumbled upon it in Vue as well after some months of using it... I guess I'm getting lazier – Adam Jagosz Sep 09 '20 at 16:09
1

You can use arrow functions inside your class as they auto bind this. You can write your class methods as:

 getDocs = (cb) => {
    // console.log(this) will not returns null
    docs
      .query(qb => {
         qb.where('id', this.id);
      })
      .fetch()
      .then(function(documents) {
        cb(null, documents);
      })
    ;
 }

see this MDN article: "Arrow functions capture the this value of the enclosing context"

Federkun
  • 36,084
  • 8
  • 78
  • 90
ziv
  • 3,641
  • 3
  • 21
  • 26
1

Consider updating the body of process() to this:

process() {
  // console.log(this) returns { id: id }
  async.waterfall([
    (cb)=>{this.getDocs(cb);},
    (documents,cb)=>{this.alterDocs(documents,cb);},
    (cb)=>{this.reindexSearch(cb);}
  ]);
}

Using arrow functions ensures that the member functions of the class get invoked with the correct context.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
0

I created myself the following functions.

let bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; //from coffeescript's => operator

//use in a class's constructor to make the this pointer always refer to the object in which a function resides
function fixThisPointer(_this, func){
  _this[func.name] = bind(_this[func.name], _this);
}

function fixThisPointer2(_this, funcNameArray){
  for (name of funcNameArray){
    _this[name] = bind(_this[name], _this);
  }
}

And then, when I need it, I use this command in my constructors

fixThisPointer(this, foo)

Or this command

fixThisPointer2(this, ['foo', 'foo2', 'foo3'])
hostingutilities.com
  • 8,894
  • 3
  • 41
  • 51