7

Suppose I have a class (very simple scenario)

class Student
{
    name = "John";

    sayHello()
    {
        console.log("Hi, I'm " + this.name);
    }
}

It's compiled by TypeScript compiler to:

var Student = (function () {
    function Student() {
        this.name = "John";
    }
    Student.prototype.sayHello = function () {
        console.log("Hi, I'm " + this.name); //here is the problem. Accessing name via this
    };
    return Student;
})();

Now if I create an object and call a method, everything works fine.

var student = new Student();

student.sayHello(); //prints Hi, I'm John

But if I invoke that method from callback, it breaks (this is referencing a Window as expected)

setTimeout(student.sayHello); //prints Hi, I'm 

I'm aware of the difference between this in JavaScript and C# or Java. I'm also aware, that TypeScript tries to address this difference. For example this code:

class Student
{
    name = "John";

    sayHelloTo(other)
    {
        other(() => this.name);
    }
}

Would have been compiled to

var Student = (function () {
    function Student() {
        this.name = "John";
    }
    Student.prototype.sayHelloTo = function (other) {
        //note, the compiler solves the problem by capturing this into local variable
        var _this = this; 
        other(function () {
            return _this.name;
        });
    };
    return Student;
})();

Why isn't the compiler creates something like _this variable in the first scenario for class members? I would expect to see something along next code (not a real output and this code is not correct either, just to show my intention)

var Student = (function () {
    var _this;
    function Student() {
        _this = this; //solves the problem of setTimeout(student.sayHello)
        _this.name = "John";
    }
    Student.prototype.sayHello = function () {
        console.log("Hi, I'm " + _this.name);
    };
    return Student;
})();

I've used the TypeScript v0.9.7 compiler

Ilya Ivanov
  • 23,148
  • 4
  • 64
  • 90
  • I'm not sure if `javascript` tag is usable here. – Ilya Ivanov Mar 25 '14 at 13:15
  • It wouldn't do any good. What would the value of `_this` be set to? – Pointy Mar 25 '14 at 13:17
  • @Pointy to correct value of `this`. I've executed that example and it works fine. I'm probably missing something global in JavaScript part, but that specific class example works fine for `student.sayHello()` and `setTimeout(student.sayHello)` – Ilya Ivanov Mar 25 '14 at 13:19
  • Try it after creating **two** students, and after setting the "name" property of one of them to "Peter". – Pointy Mar 25 '14 at 13:20
  • I see your point. I completely agree, that my last code is **not correct**, I'm just showing the direction, where I see the issue. I can't propose a solution for this problem – Ilya Ivanov Mar 25 '14 at 13:22
  • possible duplicate of [this inside prototype function equal to window instead of object instance](http://stackoverflow.com/questions/22323585/this-inside-prototype-function-equal-to-window-instead-of-object-instance) – Fenton Mar 25 '14 at 17:00

2 Answers2

1

The only thing that the compiler could do would be to make sure each constructed object had a bound copy of the prototype functions. That would involve a very significant semantic change, so it can't really do that.

The translated code returns a function that has access to a closure, it's true. However, in your suggested alternative, there's only one _this that would be shared by all instances created by the constructor. The closure is in that function that is called to create the "Student" constructor; that function only runs once, when the constructor is made, and then never again. Thus each call to new Student() would update that single, shared variable _this. (In the example, the way that would cause a problem would be for the "name" property to change on a Student instance. If they all are named "John" it doesn't matter :)

The fundamental issue is that in JavaScript, there is no intrinsic relationship between a function and any object. When you call

setTimeout(student.sayHello, 100);

the first parameter expression evaluates to a plain reference to that "sayHello" function. The fact that the reference came from the object is lost. I suppose another alternative for Typescript would be to catch those sorts of expressions and create a bound function at that point. That is, the class code itself would remain the same, but the setTimeout() call would be translated as

setTimeout(student.sayHello.bind(student), 100);

What sort of ramifications that would have on everything I can't say. I also don't know how hard it would be for the compiler to know that it should do that transformation; there might be times at which it doesn't make sense.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • thanks for a response. Could you please show how is `_this` is shared along all the instances? I just don't know how this is possible, if I have a `var _this;` within a self invoking function. I would be very thankful for an example – Ilya Ivanov Mar 25 '14 at 13:24
  • @IlyaIvanov OK I'll extend the answer. – Pointy Mar 25 '14 at 13:26
  • now I see your point about single `_this`. Thanks. I'll try to understand the whole picture and then accept the answer, if you don't mind – Ilya Ivanov Mar 25 '14 at 13:29
  • Mike, could you please elaborate a bit about another answer? Isn't a `sayHello = () => console.log("Hi, I'm " + this.name);` syntax is the solution for this problem, since it creates a `_this` variable within `Student` constructor? I have little experience with JS, so I could easily miss corner cases of that language. – Ilya Ivanov Mar 25 '14 at 13:36
  • That approach is essentially doing the same thing as using `.bind()` - it is creating *another* function, and the new function explicitly maintains the relationship between the Student instance and the function. (The `.bind()` function is basically just a built-in way of doing that.) – Pointy Mar 25 '14 at 13:51
  • Sorry, I'm probably being very stupid, but I only see the difference between my first `Student` definition and Dicks first `Student` definition in the scope of the `sayHello` function declaration. In my case it is declared in type constructor, which, as you correctly noted, is called only once, while in Dick's example it is declared within the object constructor. With the bind example, you set the `owner` of the function externally to the type. – Ilya Ivanov Mar 25 '14 at 13:56
  • ok, I've got what you mean. The behavior is the same for `.bind()` example and `sayHello = () => ...`. I just didn't saw how `.bind()` can be used within class declaration in a readable way. I can declare a separate function and `.bind()` it to `sayHello`, but it can become very messy, if I'm not mistaken – Ilya Ivanov Mar 25 '14 at 14:01
  • Well Typescript apparently compiles things that look like variable initializations into simple member variables that are initialized in the constructor. Because his `_this` is a constructor variable, each call to the constructor (each `new Student()`) gets a unique `_this`. – Pointy Mar 25 '14 at 14:02
  • I was not aware of that. I was expecting to see equal JavaScript for `sayHello = () => ... ` and `sayHello{ ... }`. I will accept Dick's answer, since I'll use his approach for class declarations, where I need to pass members as callbacks. But thank you very much for patient and deep explanations. I appreciate your answer a lot. – Ilya Ivanov Mar 25 '14 at 14:06
1

You might want to change the sayHello function like below to make it generate to code you want. Notice the sayHello = () => { } This will still work with multiple students which is not the case with your example.

class Student
{
    name = "John";

    sayHello = () =>
    {
        console.log("Hi, I'm " + this.name);
    }
}

It will generate code like this:

function Student() {
    var _this = this;
    this.name = "John";
    this.sayHello = function () {
        console.log("Hi, I'm " + _this.name);
    };
}

Another possibility is to change the call to setTimeout like this

setTimeout(() => { student.sayHello() });
Dick van den Brink
  • 13,690
  • 3
  • 32
  • 43