TypeScript中是否有'this'的别名?

77

我尝试使用TypeScript编写一个类,其中定义了一个方法,作为jQuery事件的事件处理程序回调。

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This is not good.
    }
}

在onFocusIn事件处理程序中,TypeScript将'this'视为类的'this'。但是,jQuery覆盖了this引用并将其设置为与事件关联的DOM对象。

一种替代方法是在构造函数中定义Lambda作为事件处理程序,这样TypeScript会创建一个带有隐藏_this别名的闭包。

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e) => {
            var height = this.textarea.css('height'); // <-- This is good.
        });
    }
}

我的问题是,在使用TypeScript编写基于方法的事件处理程序时,是否有其他方法可以访问this引用,以克服这种jQuery行为?


3
使用 () => {} 的答案看起来不错。 - Daniel Little
12个回答

104

使用箭头函数的语法 () => { ... },可以保留this的作用域 - 这里有一个例子摘自TypeScript For JavaScript Programmers

var ScopeExample = { 
  text: "Text from outer function", 
  run: function() { 
    setTimeout( () => { 
      alert(this.text); 
    }, 1000); 
  } 
};

请注意,this.text返回的是外部函数文本,因为箭头函数语法保留了"词法作用域"。


“this”的上下文并未被您的示例修改,因此您展示的内容并不适用。 - Paul Mendoza
@PaulMendoza,实际上,如果你将它改为setTimeout( function () { alert(this.text); }, 1000);,你会得到undefined - Fenton
你是对的。我的错。不知怎么的,()=> 告诉编译器这意味着与 function() { } 不同的东西。 - Paul Mendoza
9
很棒,赞!这个方法在幕后创建了一个var _this = this,并在匿名函数中引用了_this - Richard Rout

23

正如所述,TypeScript 没有确保方法始终绑定到其 this 指针的机制(这不仅仅是 jQuery 的问题)。但这并不意味着没有一种相当简单的方法来解决这个问题。您需要生成一个代理以恢复方法调用之前的 this 指针,然后再将回调函数包裹在该代理中,再传入事件中。jQuery 内置了一个名为 jQuery.proxy() 的机制可以实现这一点。以下是使用该方法的示例代码(请注意添加的 $.proxy() 调用)。

class Editor { 
    textarea: JQuery; 

    constructor(public id: string) { 
        this.textarea = $(id); 
        this.textarea.focusin($.proxy(onFocusIn, this)); 
    } 

    onFocusIn(e: JQueryEventObject) { 
        var height = this.textarea.css('height'); // <-- This is not good. 
    } 
} 

虽然那是一个合理的解决方案,但我个人发现开发者经常会忘记包含代理调用,所以我想出了另一种基于 TypeScript 的解决方案。使用下面的 HasCallbacks 类,你只需要从 HasCallbacks 派生你的类,然后任何以 'cb_' 为前缀的方法都将永久绑定它们的 this 指针。你不能使用不同的 this 指针调用该方法,在大多数情况下这是更可取的。两种机制都可以工作,只需选择你认为更容易使用的即可。

class HasCallbacks {
    constructor() {
        var _this = this, _constructor = (<any>this).constructor;
        if (!_constructor.__cb__) {
            _constructor.__cb__ = {};
            for (var m in this) {
                var fn = this[m];
                if (typeof fn === 'function' && m.indexOf('cb_') == 0) {
                    _constructor.__cb__[m] = fn;                    
                }
            }
        }
        for (var m in _constructor.__cb__) {
            (function (m, fn) {
                _this[m] = function () {
                    return fn.apply(_this, Array.prototype.slice.call(arguments));                      
                };
            })(m, _constructor.__cb__[m]);
        }
    }
}

class Foo extends HasCallbacks  {
    private label = 'test';

    constructor() {
        super();

    }

    public cb_Bar() {
        alert(this.label);
    }
}

var x = new Foo();
x.cb_Bar.call({});

1
$.proxy() 调用是我情况下的一个很好的解决方案。使用 this 别名会涉及到复杂化 TS,并且正如其他人指出的那样,这将难以以向后兼容的方式实现。 - Todd
1
我想补充一下,我知道HasCallbacks的代码看起来有点吓人,但它所做的只是遍历您类的成员并使用代理进行预连接。如果您的项目很小,最好使用$.proxy()。然而,如果您的项目很大,HasCallbacks类将导致客户端下载更少的代码(您不需要所有这些额外的$.proxy()调用),并且出错的可能性更小。执行方面,两种方法的性能差不多(HasCallbacks对于每个类都有一次枚举开销),所以这真的取决于您的选择。 - Steven Ickman
3
我认为你的回答可能会误导人,因为它让我觉得 TypeScript 不支持这个功能,但实际上只需要使用箭头语法来定义方法就可以了。 - Sam
这个答案可能提供了一种解决方案,但并不正确,因为TypeScript确实提供了一种维护this的词法作用域的机制。请参见Sam的答案,了解使用箭头函数解决此问题的两个选项。 - bingles

21

就像其他答案所提到的那样,使用箭头语法来定义函数会导致对this引用始终指向封闭类。

因此,为了回答您的问题,这里有两个简单的解决方法。

使用箭头语法引用该方法

constructor(public id: string) {
    this.textarea = $(id);
    this.textarea.focusin(e => this.onFocusIn(e));
}

使用箭头语法定义方法

onFocusIn = (e: JQueryEventObject) => {
    var height = this.textarea.css('height');
}

6

您可以在构造函数中将成员函数绑定到其实例。

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
        this.onFocusIn = this.onFocusIn.bind(this); // <-- Bind to 'this' forever
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height');   // <-- This is now fine
    }
}

或者,只需在添加处理程序时绑定它。

        this.textarea.focusin(onFocusIn.bind(this));

这是我最喜欢的方法,但不幸的是,对于旧版浏览器,您需要一个 bind 的 polyfill。 - Antoine

4

试试这个

class Editor 
{

    textarea: JQuery;
    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e)=> { this.onFocusIn(e); });
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This will work
    }

}

3

Steven Ickman的解决方案很方便,但是不完整。Danny Becket和Sam的答案更短,更手动,并且在需要同时具有动态和词法作用域“this”的回调的同一常规情况下失败。如果我的下面的解释过长,请跳到我的代码...

我需要保留“this”以用于库回调的动态作用域,并且我需要具有与类实例相关的词法作用域“this”。我认为将实例传递给回调生成器最为优雅,从而使参数闭合类实例。编译器会告诉你是否漏掉了这样做。我使用将词法作用域参数称为“outerThis”的约定,但“self”或其他名称可能更好。

“this”关键字的使用被借用自OO世界,当TypeScript采用它时(我认为是从ECMAScript 6规范中采用的),它们混淆了一个词法作用域概念和一个动态作用域概念,每当方法由不同实体调用时。我对此有点恼火;我更喜欢在TypeScript中使用“self”关键字,以便我可以将词法作用域对象实例交给它。或者,JS可以被重新定义为在需要时需要明确的第一个位置的“caller”参数(从而一举打破所有网页)。

这是我的解决方案(从一个大类中删除)。特别注意方法的调用方式,以及“dragmoveLambda”的主体:

export class OntologyMappingOverview {

initGraph(){
...
// Using D3, have to provide a container of mouse-drag behavior functions
// to a force layout graph
this.nodeDragBehavior = d3.behavior.drag()
        .on("dragstart", this.dragstartLambda(this))
        .on("drag", this.dragmoveLambda(this))
        .on("dragend", this.dragendLambda(this));

...
}

dragmoveLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
    console.log("redefine this for dragmove");

    return function(d, i){
        console.log("dragmove");
        d.px += d3.event.dx;
        d.py += d3.event.dy;
        d.x += d3.event.dx;
        d.y += d3.event.dy; 

        // Referring to "this" in dynamic scoping context
        d3.select(this).attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

        outerThis.vis.selectAll("line")
            .filter(function(e, i){ return e.source == d || e.target == d; })
            .attr("x1", function(e) { return e.source.x; })
            .attr("y1", function(e) { return e.source.y; })
            .attr("x2", function(e) { return e.target.x; })
            .attr("y2", function(e) { return e.target.y; });

    }
}

dragging: boolean  =false;
// *Call* these callback Lambda methods rather than passing directly to the callback caller.
 dragstartLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
        console.log("redefine this for dragstart");

        return function(d, i) {
            console.log("dragstart");
            outerThis.dragging = true;

            outerThis.forceLayout.stop();
        }
    }

dragendLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void}  {
        console.log("redefine this for dragend");

        return function(d, i) {
            console.log("dragend");
            outerThis.dragging = false;
            d.fixed = true;
        }
    }

}

虽然这个实现方式有些丑陋,但它是绕过作用域劫持的好方法。+1 - Carrie Kendall

2

我曾经遇到过类似的问题。我认为在许多情况下,您可以使用 .each() 方法来保留 this 作为以后事件的不同值。

JavaScript 的方式:

$(':input').on('focus', function() {
  $(this).css('background-color', 'green');
}).on('blur', function() {
  $(this).css('background-color', 'transparent');
});

TypeScript的方式:

$(':input').each((i, input) => {
  var $input = $(input);
  $input.on('focus', () => {
    $input.css('background-color', 'green');
  }).on('blur', () => {
    $input.css('background-color', 'transparent');
  });
});

我希望这能对某些人有所帮助。

2
您可以使用js eval函数:var realThis = eval('this');

2

除了使用胖箭头lambda语法提供的this重映射便利(从向后兼容性的角度来看,这是可以允许的,因为没有现有的JS代码可以使用=>表达式)之外,TypeScript不提供任何额外的方式(超出常规JavaScript手段)来返回“真正”的this引用。

您可以在CodePlex网站上发布建议,但从语言设计的角度来看,可能没有太多可以发生的事情,因为编译器可以提供的任何明智的关键字可能已经被现有的JavaScript代码使用。


0

有比以上所有答案都更简单的解决方案。基本上,我们可以通过使用关键字“function”而不是使用“=>”结构来回退到JavaScript,这将把“this”翻译成类“this”。

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        var self = this;                      // <-- This is save the reference
        this.textarea = $(id);
        this.textarea.focusin(function() {   // <-- using  javascript function semantics here
             self.onFocusIn(this);          //  <-- 'this' is as same as in javascript
        });
    }

    onFocusIn(jqueryObject : JQuery) {
        var height = jqueryObject.css('height'); 
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接