本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧49:回调中为this提供一个type
js的this是这门语言中最让人迷惑的部分。不像变量声明的let,const 是定义决定范围,this是动态范围。this的值不取决于它在哪定义,而是取决于它在哪调用!
this经常在class中使用,同时this经常被对象的实例调用:
class C {
vals = [1, 2, 3];
logSquares() {
for (const val of this.vals) {
console.log(val * val);
}
}
}
const c = new C();
c.logSquares();
打印:
1
4
9
如果你将logSquares放到一个变量上,然后调用:
const c = new C();
const method = c.logSquares;
method();
js运行时报错:
Uncaught TypeError: Cannot read property 'vals' of undefined
问题在于:c.logSquares()做了两件事:
- 它调用了
C.prototype.logSquares - 将上面的
C.prototype.logSquares中的this绑定给c 如果你提取出了logSquares,那么this将被设置为undefined。 js可以让你完全控制这项绑定。你可以使用call设置this来解决这个问题:
const c = new C();
const method = c.logSquares;
method.call(c); // Logs the squares again
this可以绑定任何东西,一些库可以也这么做的,让this成为它们api的一部分。甚至Dom也利用了这一点:
document.querySelector('input')!.addEventListener('change', function(e) {
console.log(this); // Logs the input element on which the event fired.
});
this绑定经常出现在回调函数的上下文。例如你想在class中定义一个onClicker函数:
class ResetButton {
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick() {
alert(`Reset ${this}`);
}
}
当Button调用onClick,会警告:"Reset undefined" 通常的解决办法是:在构造函数中创建该方法的绑定版本。
class ResetButton {
constructor() {
this.onClick = this.onClick.bind(this);
}
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick() {
alert(`Reset ${this}`);
}
}
onClick(){...}声明在ResetButton.prototype上定义了一个属性。这个属性由ResetButton的实例共享。this.onClick = ...,这句话在ResetButton的实例上创建了一个onClick属性,同时将this绑定到该实例。
由于在实例上的onClick属性调用优先级高于prototype上的onClick,所以实例上的onClick就是属性上的onClick。
复杂的this有时候也能带来便利:
class ResetButton {
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick = () => {
alert(`Reset ${this}`); // "this" always refers to the ResetButton instance.
}
}
这里我们将onClick换成箭头函数,每次ResetButton的实例构造,this都会被绑定为合适的值。
看看this生成的JavaScript很有启发性:
class ResetButton {
constructor() {
var _this = this;
this.onClick = function () {
alert("Reset " + _this);
};
}
render() {
return makeButton({ text: 'Reset', onClick: this.onClick });
}
}
这和ts有什么关系?因为ts已经对this绑定进行了建模。当你需要写一个库需要在回调中调用this,你应该对this进行建模。具体的做法是:给回调函数加一个this参数:
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn.call(el, e);
});
}
this这个参数有点特殊。当你调用回调时候传入两个参数:
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn(el, e);
// ~ Expected 1 arguments, but got 2
});
}
ts会强迫你使用该函数在正确的this上下文:
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn(e);
// ~~~~~ The 'this' context of type 'void' is not assignable
// to method's 'this' of type 'HTMLElement'
});
}
当你在回调函数中使用该函数,类型就是正确的:
declare let el: HTMLElement;
addKeyListener(el, function(e) {
this.innerHTML; // OK, "this" has type of HTMLElement
});
如果使用箭头函数,就会覆盖this。ts也能捕捉这个错误:
class Foo {
registerHandler(el: HTMLElement) {
addKeyListener(el, e => {
this.innerHTML;
// ~~~~~~~~~ Property 'innerHTML' does not exist on type 'Foo'
});
}
}
记得对你的this加上类型声明