Reputation: 10119
I have an ES2015 class, call it Foo
, which has at least two member functions, bar
and baz
. In bar
there is a call to setTimeout
whose first parameter is this.baz
. Works fine up to here, I inspected it in the debugger, and this
does refer to the instance of my class. (Actually since I'm using babel, I end up with a _this = this
substitution beforehand but anyway the right thing is being passed into the setTimeout
, confirmed.)
The problem is when the setTimeout
callback fires, it calls the right function baz
, but the this
inside of baz
the value of this
refers to Window
instead. Babel tries to do a _this2 = this
at the beginning of baz
but it seems to be too late already.
So my problem appears, somewhere in between the function baz
being passed and the time it is called, it loses its this
scoping. My question is, am I doing something wrong with ES2015 or babel here? I feel like this is a common enough use case that it shouldn't require too much strain. Personally I'd like to do this all with Promise
but due to business requirements I can't add too much new JS stuff at once.
Alternatively, is there a standard idiom for passing the scope of this
as I need here? It seems really messy and counter-intuitive to have to pass the calling object of a member function in as one of its parameters.
Here's a minimal working example for reference:
class Foo{
bar(){
setTimeout(this.baz, 1000);
}
baz(){
console.log("this message should repeat roughly once per second");
this.bar();
}
}
And here's a screenshot of me using it on a very simple page, along with the error message:
Edit: I have to object to my question being marked as a duplicate. Of course I had searched seen the setTimeout
questions before asking this one. However the ES2015 and class
-based aspect of my question is relevant and important, since the ES2015 syntax transformation of classes in babel changes the apparent behavior of this
. My question was about whether there is another ES2015 design pattern to handle this, and why the intuitive class
abstraction/encapsulation was being broken by passing a member function as a first-class value to be called externally. The most significant insight I gained was gleamed from a comment below by @FelixKing which I'll repeat here for posterity (in case anyone else is wondering):
Whether or not autobind class methods (like in Python) was discussed but ultimately decided against, likely to keep consistency with the rest of the language. There is also the question whether it would be possible to autobind methods without memory/performance impact.
Upvotes: 1
Views: 745
Reputation: 7258
My question is, am I doing something wrong with ES2015 or babel here?
Actually, it's a expected JavaScript behavior and is related to how this
is assigned in the language.
Consider the code below (no ES6, no babel...):
var obj = {
key1: 'value1',
key2: function() {
console.log(this);
}
}
obj.key2(); //will print obj
var callback = obj.key2; //assigned the function reference to some random variable
callback(); //will print Window/global object
As you can see, this
is defined when the function is invoked, not when it's declared, and depends how it's being called.
That's exactly what's happening inside setTimeout
, or in any function that receives a function as a parameter:
/* fake */
function setTimeout(fnCallback, time) {
/* wait and when the time comes, call your callback like this: */
fnCallback(); //'this' will be Window/global
}
In order to pass the desired context (in the example above), we can force the context:
using .bind
:
var callback = obj.key2.bind(obj);
callback(); //will print obj
or using .call
:
var callback = obj.key2;
callback.call(obj); //will print obj
Or we can pass an anymous function an call our object from inside:
setTimeout(function() {
//here, 'this' is Window/global, because the anonymous function is being called from a callback assignment
obj.key2(); //will print obj
}, 3000);
So, in your example, in order to properly set the setTimeout
callback and ensure that baz()
will receive the class context, you can:
bind the function when setting it as a callback:
setTimeout(this.baz.bind(this), 1000);
in your class constructor, bind
the baz
method once; so, everytime it's called, will be assigned the class context. Like this:
class Foo{
constructor() {
this.baz = this.baz.bind(this)
}
bar(){
setTimeout(this.baz, 1000);
}
baz(){
console.log("this message should repeat roughly once per second");
this.bar();
}
}
Use arrow functions
. Another way of specifying the this
context is using arrow functions
, that, actually, ensure the this
assignment is done through lexical scope (not anymore in the function invocation, but in the function declaration).
setTimeout(() => this.baz(), 1000);
// ^^^^
// 'this' here is your class, will pass your class as 'this'
// to the baz() method, due to the dot before
Different from:
setTimeout(function() { this.baz(); }, 1000);
// ^^^^
// 'this' here is Window/global, will thrown undefined method
Upvotes: 1