简体   繁体   中英

Keep babel class as `this` when using a member function in `setTimeout`

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.

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
}

"Workarounds":

In order to pass the desired context (in the example above), we can force the context:

  1. using .bind :

     var callback = obj.key2.bind(obj); callback(); //will print obj
  2. 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);

In your example

So, in your example, in order to properly set the setTimeout callback and ensure that baz() will receive the class context, you can:

  1. bind the function when setting it as a callback:

     setTimeout(this.baz.bind(this), 1000);
  2. 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(); } }
  3. 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM