1

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:

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.

machine yearning
  • 9,889
  • 5
  • 38
  • 51
  • 2
    Try `setTimeout(this.baz.bind(this), 1000);` – elclanrs Feb 08 '17 at 02:42
  • That's an interesting workaround and it seems to work fine. So it seems like the problem must just be my usage of `setTimeout` which is a function that respects the dynamic scoping of `this`, whereas I'm trying to just use it lexically in the context of `Foo`. So maybe `setTimeout` is just breaking my assumed abstraction? In your experience is this a common problem? – machine yearning Feb 08 '17 at 02:49
  • So to clarify, I need to manually bind `this` if I'm passing a member function as a first-class value to and expecting it to be called later? Just want to confirm this is a general design pattern instead of a fluke in my code. – machine yearning Feb 08 '17 at 03:00
  • @AndyRay The context matters because babel does syntax transformations that change the behavior of `this` in member functions. Apparently this behavior is overridden when you call the member function as a first class value. My question is not a duplicate of the question you've marked. Please mark duplicates more carefully. – machine yearning Feb 08 '17 at 04:33

1 Answers1

1

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
    
mrlew
  • 7,078
  • 3
  • 25
  • 28
  • Sure I understand in general how dynamic scoping works in traditional javascript (shoutout to the "You Don't Know JS: Scope & Closures" book). But the distinction that I'm trying to make is that `this` inside of a class feels like it is supposed to be **lexically** scoped to refer to an instance of the relevant class, and for the most part Babel handles this nicely. The ES6 and Babel stuff is an important part of my question. – machine yearning Feb 08 '17 at 02:56
  • @machineyearning I get it, but Babel respect `this` assignment from the language. For instance, when programming with React and ES6 classes, we always have to bind the class method to the event assigment, or `this` will never work when the callback function is triggered. – mrlew Feb 08 '17 at 03:01
  • Okay, thanks for your reply and the additional context. Just as a sanity check, am I crazy for thinking a member function should always preserve `this` as a reference to the object in which it was instantiated? Even if the member function is called from elsewhere? Edit, I can see the use case for the other situation also, but I feel like it would be the exception rather than the rule in class-based object oriented programming. – machine yearning Feb 08 '17 at 03:07
  • @machineyearning, it is not about scope, or classes, or object oriented programming really. Think of `this` as just one more (implicit) argument of a function. So a `function f(a, b)` is really just `function f(this, a, b)` and when you call it with parens only like `f(a, b)` then `this === undefined`. If you want to set the value you use `call`, `apply` or `bind`. And when the call has a dot in it like `obj.f(a, b)` then `this === obj` inside of `f`. – elclanrs Feb 08 '17 at 03:19
  • But that's simply not true in the case of ES6 member functions, where `this` is intended to refer to the object from which the function is being called. For example in my above sample code, if I called `baz` first, it would successfully call `bar` once but break after returning to `baz` through the `setTimeout` callback, where `this` for the member function has now changed (via dynamic scoping as you said). – machine yearning Feb 08 '17 at 03:24
  • Because there is a dot in the caller. `this` in `this.bar()` will refer to the object before the dot in the caller of the `baz` function. So `var foo = new Foo; foo.baz()`, see `foo` is `this` inside of `baz`, so `this.bar()` resolves to `foo.bar()`. In the `setTimeout` it will call your function like this `f()`, no dot, which means no `this`, so you have to bind it, which sets `this` forever. – elclanrs Feb 08 '17 at 03:27
  • @machineyearning I edited the answer with some "solutions". When I first learned this, I got a "wtf" moment, but then, with the time, I realized that, in fact, JavaScript is not a truly **object oriented language**: it's a **prototype-based language**. And, also, the `class` keyword in JavaScript does not create real classes (like Java class, for instance). In fact, it's a *syntatic sugar* for and old pattern (constructor, prototype object...). ES6 classes try to give some class *aspect*, but it still is the old JavaScript functions behind. – mrlew Feb 08 '17 at 03:36
  • Great answer, I appreciate the explanation in your update. I like the look of the arrow function sugar version, but it bugs me that it changes the semantics by nesting the result of `this.baz()` in an additional function call. I think I'll go with your initial solution of explicitly rebinding for now. I guess I set my hopes too high that ES6 `class` would get rid of the weird quirks of the traditional js class pattern. I guess the benefit is that I can rely on a standard implementation now, and I don't have to roll my own or rely on any external libraries to do it for me. – machine yearning Feb 08 '17 at 04:46
  • @machineyearning agreed about the arrow function. I prefer the constructor version (we use it a lot in React projects - [components](https://facebook.github.io/react/tutorial/tutorial.html#what-is-react) are built with ES6 class). As a side note: if you paste your example into the [babel editor](http://babeljs.io/repl/), you'll see that basically the class implementation uses IIFE and function constructor pattern. – mrlew Feb 08 '17 at 05:16
  • @machineyearning: 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. – Felix Kling Feb 08 '17 at 05:34
  • *"JavaScript is not a truly object oriented language"* I guess that depends on your definition of "object oriented language" ;) – Felix Kling Feb 08 '17 at 05:35
  • @FelixKling let me rephrase myself: *not a classical object oriented language* :) – mrlew Feb 08 '17 at 05:52
  • @FelixKling Aha thanks for your insight. That's actually what I was ultimately wondering, and I figured it came down to keeping the integrity of the language at the expense of offering true support of a new programming paradigm. Judging from past where a language tried to incorporate a new flavor-of-the-month paradigm at the expense of integrity, I believe that the JS language engineers chose well, despite it being confusing in this context. Hopefully other ES6 features such as Promises and arrow functions mitigate this pitfall via abstractions that are compatible with class-based programming – machine yearning Feb 08 '17 at 23:27