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)
]);
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.
I would say that best solution is to use it correctly. That means:
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).
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);
}
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