JavaScript 的 this 关键字是整个语言中最令人困惑的部分之一。与 let 和 const 声明的变量是具有词法作用域的,与之不同的是,this 是动态作用域:它的值不取决于它怎么定义,而取决于它怎么调用
要记住的事情:
- 了解 this 绑定的工作原理
- 当 this 成为你的 API 的一部分时,应该在回调中为它提供一个类型。
this最常用于类中,通常指向对象的当前实例:
class C {vals = [1, 2, 3];logSquares() {for (const val of this.vals) {console.log(val * val);}}
}const c = new C();
c.logSqures(); // 输出 1 4 9
如果你把 logSquares 放在一个变量中并调用它会发生什么:
const c = new C();
const method = c.logSqures();
method(); // ~~ uncaught TypeError: Cannot read property 'vals' of undefined
问题在于,c.logSqures() 实际上做了两件事:调用 C.prototype.logSqures,并且将该函数中 this 的值绑定到 c 上。通过引出一个对 logSquares 的引用,你把两件事分开了,this 被设置为 undefined。
JavaScript 允许你完全控制this的绑定。你可以使用 call 来显式地设置 this 并解决这个问题:
const c = new C();
const method = c.logSqures();
method.call(c);
没有理由让 this 必须绑定到 C 的实例,它可以被绑定到任何东西上。所以,库可以将 this 的值作为其 API 的一部分,甚至 DOM 也利用了这一点。例如,在一个事件处理程序中:
document.querySelector('input').addEventListener('change', function(e) {console.log(this); // 记录事件触发时的输入元素
}
this 绑定经常出现在这样的回调中。例如,如果你想在一个类中定义一个 onClick 处理程序,你可以这样:
class ResetButton {render() {return makeButton({text: 'Reset', onClick: this.onClick});}onclick() {alert(`Reset ${this}`);}
}
当 Button 调用 onClick 时,它会弹出 “Reset undefined”。同样的,问题出在 this 绑定上。一个常见的解决方案是在构造函数中创建方法的绑定:
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 实例属性排在 onClick 原型属性之前,所以 this.onClick 指的是 render() 方法中已绑定 this 的函数。
绑定有一个快捷方法,使用箭头函数:
class ResetButton {render() {return makeButton({text: 'Reset', onClick: this.onClick});}onclick = () => {alert(`Reset ${this}`); // “this”永远指向 ResetButton 实例}
}
看一下生成的 JavaScript 理解一下:
class ResetButton {constructor() {var _this = this;this.onClick = function() {alert("Reset " + _this);};}render() {return makeButton({text: 'Reset', onClick: this.onClick});}
}
那么这一切和 TypeScript 有什么关系呢?因为 this 绑定是 JavaScript 的一部分,TypeScript 需要对它进行建模。
你可以添加一个this参数到你的回调中:
function addKeyListener(el: HTMLElement,fn: (this: HTMLElement, e: KeyboardEvent) => void
) {el.addEventListener('keydown', e => {fn(el, e); // ~ 期待输入1个参数,但得到2个});
}
更好的是, TypeScript 会强制要求你用正确的 this 上下文来调用函数:
function addKeyListener(el: HTMLElement,fn: (this: HTMLElement, e: KeyboardEvent) => void
) {el.addEventListener('keydown', e => {fn(el); // ~~ 类型为“void” 上下文不可分配给类型为“HTMLElement” 的方法“this”});
}
作为这个函数的使用者,你可以在回调中引用 this,并获得完全的类型安全:
declare let el: HTMLElement;
addKeyListener(el, function(e) {this.innerHTML; // OK,“this”的类型是 HTMLElement
});
当然,如果你在这里使用一个箭头函数,this 就会被覆盖。TypeScript 会捕获这个问题:
class Foo {registerHandler(el: HTMLElement) {addKeyListener(el, e => {this.innerHTML; // ~~ 类型“Foo”上不存在属性“innerHTML”});}
}
不要忘记 this! 如果你在回调中设置 this 的值,那么它就是你的 API 的一部分,就应该在你的类型声明中包含它。