如何在TypeScript中输入this

84 阅读6分钟

有时在写JavaScript时,我想大喊 "这太荒谬了!"。但是我又从来不知道this 是指什么

如果说在JavaScript中,有一个概念让人困惑,那一定是this 。特别是如果你的背景是基于类的面向对象的编程语言,在那里this 总是指一个类的实例。JavaScript中的this 完全不同,但不一定更难理解。有一些基本的规则,以及同样多的例外情况需要记住。而TypeScript可以提供很大的帮助!

这在常规的JavaScript函数中#

我喜欢思考this 的一种方式是,在常规函数中(使用function 关键字或对象函数的简称),解析到 "最近的对象",也就是它们所绑定的对象。比如说。

const author = {
  name: "Stefan",
  // function shorthand
  hi() {
    console.log(this.name);
  },
};

author.hi(); // prints 'Stefan'

在上面的例子中,hi 被绑定到author ,所以thisauthor

JavaScript很灵活,你可以在运行中附加函数或将函数应用到一个对象上。

const author = {
  name: "Stefan",
  // function shorthand
  hi() {
    console.log(this.name);
  },
};

author.hi(); // prints 'Stefan'

const pet = {
  name: "Finni",
  kind: "Cat",
};

pet.hi = author.hi;

pet.hi(); // prints 'Finni'

"最近的对象 "是pethi 被绑定到pet

我们可以独立于对象来声明一个函数,并且仍然可以在对象上下文中用applycall

function hi() {
  console.log(this.name);
}

const author = {
  name: "Stefan",
};

const pet = {
  name: "Finni",
  kind: "Cat",
};

hi.apply(pet); // prints 'Finni'
hi.call(author); // prints 'Stefan'

最近的对象是我们作为第一个参数传递的对象。文档中称第一个参数为thisArg ,所以这个名字已经告诉了你应该期待什么。

应用与调用#

callapply 之间有什么区别?想想看,一个带有参数的函数。

function sum(a, b) {
  return a + b;
}

使用call ,你可以一个一个地传递参数。

sum.call(null, 2, 3);

null 是和应该被绑定的对象,所以没有对象。

使用apply ,你必须以数组的形式传递参数。

sum.apply(null, [2, 3]);

记住这种行为的一个简单的记忆法是:数组代表应用逗号代表调用

绑定#

另一种明确地一个对象绑定到一个无对象函数的方法是使用bind

const author = {
  name: "Stefan",
};

function hi() {
  console.log(this.name);
}

const boundHi = hi.bind(author);

boundHi(); // prints 'Stefan'

这已经很酷了,但后面会有更多的介绍。

事件监听器#

当你使用事件监听器时,"最近的对象 "的概念有很大的帮助。

const button = document.querySelector("button");

button.addEventListener("click", function () {
  this.classList.toggle("clicked");
});

this 是 。 设置许多 函数中的一个。另一种方式是button addEventListener onclick

button.onclick = function () {
  this.classList.toggle("clicked");
};

这使得在这种情况下,thisbutton 的原因更明显一些。

这在箭头函数和类中#

所以我花了一半的专业JavaScript生涯来完全理解this 指的是什么,只是为了看到类和箭头函数的兴起,把一切又颠覆了。

下面是我最喜欢的这个备忘录 Two people shouting at each other: This is the difference between arrow functions and normal functions. What is the difference? This is the difference? And so on, and so on.

我最喜欢的这个备忘录

箭头函数总是根据其词法范围来解决this 。词法范围意味着内部范围与外部范围相同,所以this ,在一个箭头函数内与在一个箭头函数外是相同的。比如说。

const lottery = {
  numbers: [4, 8, 15, 16, 23, 42],
  el: "span",
  html() {
    // this is lottery
    return this.numbers
      .map(
        (number) =>
          //this is still lottery
          `<${this.el}>${number}</${this.el}>`
      )
      .join();
  },
};

调用lottery.html() ,得到的是一个所有数字都包裹在跨度中的字符串,因为thismap 的箭头函数里面并没有改变。它仍然是lottery

如果我们使用一个普通的函数,this 将是未定义的,因为没有最近的object 。我们将不得不绑定this

const lottery = {
  numbers: [4, 8, 15, 16, 23, 42],
  el: "span",
  html() {
    // this is lottery
    return this.numbers
      .map(
        function (number) {
          return `<${this.el}>${number}</${this.el}>`;
        }.bind(this)
      )
      .join("");
  },
};

这很麻烦。

在类中,this 也指的是词法范围,也就是类的实例。现在我们要开始使用Java了!

class Author {
  constructor(name) {
    this.name = name;
  }

  // lexical, so Author
  hi() {
    console.log(this.name);
  }

  hiMsg(msg) {
    // lexical, so still author!
    return () => {
      console.log(`${msg}, ${this.name}`);
    };
  }
}

const author = new Author("Stefan");
author.hi(); //prints '
author.hiMsg("Hello")(); // prints 'Hello, Stefan'

解除绑定#

如果你不小心解除了对一个函数的绑定,例如,通过传递一个被绑定在其他函数上的函数或者将其存储在一个变量中,就会出现问题。

const author = {
  name: "Stefan",
  hi() {
    console.log(this.name);
  },
};

const hi = author.hi();
// hi is unbound, this refers to nothing
// or window/global in non-strict mode
hi(); // 💥

你将不得不重新绑定该函数。这也解释了React类组件中带有事件处理程序的一些行为。

class Counter extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 1,
    };
  }

  // we have to bind this.handleClick to the
  // instance again, because after being
  // assigned, the function loses its binding ...
  render() {
    return (
      <>
        {this.state.count}
        <button onClick={this.handleClick.bind(this)}>+</button>
      </>
    );
  }

  //... which would error here as we can't
  // call `this.setState`
  handleClick() {
    this.setState(({ count }) => ({
      count: count + 1,
    }));
  }
}

这在TypeScript中#

TypeScript在寻找 "最近的对象 "或了解词法范围方面相当出色,所以TypeScript可以给你准确的信息,让你从this 。然而,在一些边缘情况下,我们可以提供一点帮助。

这个参数#

可以考虑将一个事件处理函数提取为它自己的函数。

const button = document.querySelector("button");
button.addEventListener("click", handleToggle);

// Huh? What's this?
function handleToggle() {
  this.classList.toggle("clicked"); //💥
}

我们失去了所有关于this 的信息,因为this 现在会变成windowundefined 。TypeScript也给了我们红色的方块字!

我们在函数的第一个位置添加一个参数,在这里我们可以定义this 的类型。

const button = document.querySelector("button");
button.addEventListener("click", handleToggle);

function handleToggle(this: HTMLElement) {
  this.classList.toggle("clicked"); // 😃
}

这个参数在编译后会被删除。我们现在知道this 将是HTMLElement 的类型,这也意味着一旦我们在不同的环境中使用handleToggle ,就会出现错误。

// The 'this' context of type 'void' is not
// assignable to method's 'this' of type 'HTMLElement'.
handleToggle(); // 💥

ThisParameterType和OmitThisParameter#

如果你在函数签名中使用this 参数,有一些帮助工具。

ThisParameterType 告诉你希望 是哪种类型。this

const button = document.querySelector("button");
button.addEventListener("click", handleToggle);

function handleToggle(this: HTMLElement) {
  this.classList.toggle("clicked"); // 😃
  handleClick.call(this);
}

function handleClick(this: ThisParameterType<typeof handleToggle>) {
  this.classList.add("clicked-once");
}

OmitThisParameter 删除 ,并给你一个函数的空白类型签名。this

// No reason to type `this` here!
function handleToggle(this: HTMLElement) {
  console.log("clicked!");
}

type HandleToggleFn = OmitThisParameter<typeof handleToggle>;

declare function toggle(callback: HandleToggleFn);

toggle(function () {
  console.log("Yeah works too");
}); // 👍

ThisType#

还有一个通用的辅助类型,帮助为对象定义this ,叫做ThisType 。它最初来自于Vue等处理对象的方式。比如说。

var app5 = new Vue({
  el: "#app-5",
  data: {
    message: "Hello Vue.js!",
  },
  methods: {
    reverseMessage() {
      // OK, so what's this?
      this.message = this.message.split("").reverse().join("");
    },
  },
});

reverseMessage() 函数中看this 。正如我们所学到的,this 指的是最近的对象,这将是methods 。但是Vue把这个对象转化为不同的东西,所以你可以访问data 中的所有元素和methods 中的所有方法(例如:this.reverseMessage() )。

通过ThisType ,我们可以在这个特定位置声明this 的类型。

上面的代码的对象描述符看起来是这样的。

type ObjectDescriptor<Data, Methods> = {
  el?: string;
  data?: Data;
  methods?: Methods & ThisType<Data & Methods>;
};

它告诉TypeScript,在methods 的所有函数中,它可以访问DataMethods 的字段。

输入这个极简版本的Vue看起来是这样的。

declare const Vue: VueConstructor;

type VueConstructor = {
  new<D, M>(desc: ObjectDescriptor<D, M>): D & M
)

ThisType<T> 在 本身是空的。它是一个标记,供编译器将 指向另一个对象。正如你在lib.es5.d.ts this 这个操场上看到的, 正是它应该有的。this

底线#

我希望这篇关于this 的文章确实阐明了JavaScript中不同的怪癖以及如何在TypeScript中输入this