Search code examples
node.jsecmascript-6babeljs

ES6 methods get a null "this" and class variables are inaccessible


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)
    ]);

Solution

  • Edit 2024 - because its "still a thing" I am extending the answer with more details about the issue.

    (The original answer is moved at the end of this post)

    There are multiple solutions.

    1. Use it correctly

    I would say that best solution is to use it correctly. That means:

    • never take function "out of its instance". If you need to call that function later, just keep the whole instance, for example like this:

    class A {
      constructor(id) {
        this.id = id;
      }
    
      someFunction(){
        console.log(this);
      }
    }
    
    const a1 = new A(5);
    const a2 = new A(20);
    const arr = [a1, a2];
    
    // some business logic
    
    arr.forEach(a => a.someFunction());

    • do not use this and classes if not necessary. When you require files like repositories, controllers, services, it by default creates singletons, which is something you want (its basically like @Autowired in Java, but native).

      • For example somethingService.js can look similar to the code below. Then you dont have to think about using doSomething() as somethingService.doSomething() or you require just the function itself and use doSomething() directly.
    const someDefaultValue = 10;
    
    export function doSomething() {
       console.log(someDefaultValue);
    }
    
    1. Use apply/call/bind

    I am personally not fan of using apply (or call or bind which is basically the same, just called a bit differently) extensively, from my experience it can bring even more chaos into code. But there can be situations where you might need it, so I will list it here. You can basically "inject" this with whatever object you like inside function.

        class Simple { 
          constructor(id) {
            this.id = id;
            self = this;
          }
    
          showThis(text) {
            console.log(this, ` ** ${text} **`)
          }
        }
        
        
    const simple = new Simple(25);
    const showThisFn = simple.showThis;
    showThisFn("using it without apply");
    showThisFn.apply(simple, ["using it with apply"])
    showThisFn.call(simple, "using it with call - basically same as apply, only syntactic sugar difference today")
    boundShowThisFn = showThisFn.bind(simple);
    boundShowThisFn("now it is bind with the instance, so you dont have to specify it when calling it");


    There is one more possible issue with this, but its easily resolved by using Arrow function.

    Use Arrow Function => correctly

    If you have function inside function and you use this, the arrow function will pass the this context in the way you would expect. Using function keyword will change the context of this.

    (basically use arrow functions whenever it is possible and you are safe)

    class A {
      constructor(baseValue) {
        this.baseValue = baseValue;
      }
    
      doMathArrowFunction(arr){
        arr.forEach(item => {
          console.log(`Arrow function: item is ${item} and this?.baseValue is ${this?.baseValue}`);
          console.log(item + this?.baseValue);
        });
      }
          
    
      doMathOldWay(arr) {
        arr.forEach(function(item) {
          console.log(`Old fashion function: item is ${item} and this?.baseValue is ${this?.baseValue}`);
          console.log(item + this?.baseValue);
        });
      }
    }
     
    
    const x = [1,2];
    const a = new A(10);
    a.doMathArrowFunction(x);
    a.doMathOldWay(x);

    In past (before Arrow function existed) it was often resolved with self/that keywords. If you encounter it (most likely in some very old code) and want to understand it more - you can check the accepted answer in this post: Maintaining the reference to "this" in Javascript when using callbacks and closures


    ORIGINAL ANSWER

    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 undefined, which is calling "docsAsFunction", because you did not call that function directly from the instance of a class, therefore context is different.

    You can read about it for example here