[译]<<Effective TypeScript>> 技巧49:回调中为this提供一个type

208 阅读3分钟

本文的翻译于<<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()做了两件事:

  1. 它调用了C.prototype.logSquares 
  2. 将上面的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加上类型声明