4

In the following code in HeadDirective.prototype.link, this is equal to the global window object rather than the HeadDirective instance. My understanding is that the value of this inside a prototype function is the containing object itself.

var HeadDirective = (function () {
    function HeadDirective($rootScope, $compile) {
        this.$rootScope = $rootScope;
        this.$compile = $compile;
        this.restrict = 'E';
    }
    HeadDirective.prototype.link = function (scope, elem) {
        var html = '<link rel="stylesheet" ng-repeat="cssUrl in routeStyles" ng-href="{{cssUrl}}" />';
        elem.append(this.$compile(html)(scope));
        scope.routeStyles = [];
        this.$rootScope.$on('$routeChangeStart', function (e, next, current) {
            if (next && next.$$route && next.$$route.css) {
                if (!Array.isArray(next.$$route.css)) {
                    next.$$route.css = [next.$$route.css];
                }
                angular.forEach(next.$$route.css, function (sheet) {
                    scope.routeStyles.push(sheet);
                });
            }
        });
        this.$rootScope.$on('$routeChangeSuccess', function (e, next, current) {
            if (current && current.$$route && current.$$route.css) {
                if (!Array.isArray(current.$$route.css)) {
                    current.$$route.css = [current.$$route.css];
                }
                angular.forEach(current.$$route.css, function (sheet) {
                    scope.routeStyles.splice(scope.routeStyles.indexOf(sheet), 1);
                });
            }
        });
    };
    return HeadDirective;
})();

directives.directive('head', [
    '$rootScope', '$compile', function ($rootScope, $compile) {
        return new HeadDirective($rootScope, $compile);
    }]);

The above code was generated from the following TypeScript:

class HeadDirective implements ng.IDirective {

    constructor(private $rootScope: ng.IRootScopeService, private $compile: ng.ICompileService) {}

    link(scope: IScope, elem: JQuery): void {
        var html = '<link rel="stylesheet" ng-repeat="cssUrl in routeStyles" ng-href="{{cssUrl}}" />';
        elem.append(this.$compile(html)(scope));
        scope.routeStyles = [];
        this.$rootScope.$on('$routeChangeStart', (e: ng.IAngularEvent, next?: IRoute, current?: IRoute): any => {
            if(next && next.$$route && next.$$route.css){
                if(!Array.isArray(next.$$route.css)){
                    next.$$route.css = [next.$$route.css];
                }
                angular.forEach(next.$$route.css, (sheet: string) => {
                    scope.routeStyles.push(sheet);
                });
            }
        });
        this.$rootScope.$on('$routeChangeSuccess', (e: ng.IAngularEvent, next?: IRoute, current?: IRoute): any => {
            if(current && current.$$route && current.$$route.css){
                if(!Array.isArray(current.$$route.css)){
                    current.$$route.css = [current.$$route.css];
                }
                angular.forEach(current.$$route.css, (sheet) => {
                    scope.routeStyles.splice(scope.routeStyles.indexOf(sheet), 1);
                });
            }
        });
    }

    restrict = 'E';
}

directives.directive('head', ['$rootScope','$compile', ($rootScope: ng.IRootScopeService, $compile: ng.ICompileService): ng.IDirective =>{
    return new HeadDirective($rootScope, $compile);
}]);

According to the latest TypeScript language specification:

The type of this in an expression depends on the location in which the reference takes place:

  • In a constructor, instance member function, instance member accessor, or instance member variable initializer, this is of the class instance type of the containing class.
  • In a static member function or static member accessor, the type of this is the constructor function type of the containing class.
  • In a function declaration or a standard function expression, this is of type Any.
  • In the global module, this is of type Any.

In all other contexts it is a compile-time error to reference this.

The TypeScript language specification is quite clear. Inside a member function (which is compiled into a prototype function), this refers to the class instance. This is obviously not what I'm seeing.

Any ideas? Could Browserify be interfering with this?

Steve
  • 8,066
  • 11
  • 70
  • 112
  • A video on `this` in typescript : https://www.youtube.com/watch?v=tvocUcbCupA&hd=1 – basarat Apr 21 '14 at 05:38
  • Possible duplicate of [How to access the correct \`this\` inside a callback?](https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback) – Heretic Monkey Oct 26 '18 at 14:22

1 Answers1

6

The this keyword is highly contextual. If a method is called by an event, this will be the object that is the event target, for example.

You can get around this problem by shimmying this into a variable, or by using the JavaScript call (or apply) methods to bind the scope of this.

Short example... here is the premise:

class MyClass {
    constructor(private myProp: string) {

    }

    myMethod() {
        alert(this.myProp);
    }
}

var myClass = new MyClass('Test');

// 'Test'
myClass.myMethod();

// undefined
window.setTimeout(myClass.myMethod, 1000);

Solution One - Arrow Syntax

In TypeScript the arrow syntax will shimmy this into a variable called _this automatically for you and substitute usages inside the arrow function... So this will solve the undefined issue above and instead alert Test.

class MyClass {
    constructor(private myProp: string) {

    }

    public myMethod = () => {
        alert(this.myProp);
    }
}

Solution Two - Call Method

You can use the call method to replace the contextual this with any object you like, in the example below we reset it to be the myClass instance.

This works whether you are writing TypeScript or plain JavaScript... whereas the first solution is really a TypeScript solution.

window.setTimeout(function() { myClass.myMethod.call(myClass) }, 1000);

Or to be shorter (to be clear, the use of the arrow function here has nothing to do with scope - it is just a shorter syntax arrow functions only affect scope if you have this inside of them):

window.setTimeout(() => myClass.myMethod.call(myClass), 1000);
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Would you say, then, that the TypeScript language specification makes a promise it cannot keep? – Steve Mar 11 '14 at 12:09
  • 2
    The spec is describing the *compile-time type* of the `this` keyword in the section you quoted, not the value. Because TypeScript has the same `this` semantics as JavaScript at runtime, it's always going to be possible to end up with the 'wrong' value. – Ryan Cavanaugh Mar 11 '14 at 14:28
  • OK. If `this` inside an object's constructors, methods, accessors and mutators isn't always equal to the class instance itself, then there is no point having classes. If I want JavaScript semantics, I'll use JavaScript. – Steve Mar 11 '14 at 23:43
  • @SteveTaylor I understand your frustration - but actually you will want to choose when to use `this` as the instance and `this` as the context. Event handling, for example, works much better if you can use `this` in context. – Fenton Mar 12 '14 at 09:27
  • Yes, I want to at least be able to **choose** without ugly hacks (e.g. shoving everything into the constructor, thereby not being able to implement interfaces and generally making the `class` and constructor keywords no better than comments). Failing choice, I much prefer what all other languages with classes have, which is a built-in, guaranteed method of accessing an instance from its own **instance** methods. – Steve Mar 12 '14 at 09:42
  • My second solution leaves the class entirely untouched. It is less defensive, but the class design is then left "pure". – Fenton Mar 12 '14 at 09:50
  • Silly me - I just re-read **Solution One** and realized it's what I'm looking for - a way to write instance methods where `this` is guaranteed to be the instance method's instance. If I had read it properly, I wouldn't have had to independently discover it was added in TypeScript 0.9.1. Thanks! – Steve Mar 12 '14 at 10:04
  • No problems - glad it helped :) – Fenton Mar 12 '14 at 14:59