回归基础:理解并掌握 JavaScript 中的“this”
最近我一直在思考this
很多事情,因为我的 Web 代码中有很多链式回调函数。这是一个很好的机会,可以让我回归基础,回顾一下this
JavaScript 的工作原理,以及有哪些工具可以解决它的怪癖。
对于从 Java 或 Swift 等更典型的面向对象语言转行过来的新开发者来说,JavaScript 对this
关键字的奇怪使用是一个陷阱,随时可能让你的代码崩溃。如果你使用 React 的类组件,这种情况尤其危险,因为你经常在类中定义方法作为回调处理程序。如果你盲目地认为它this
会按照你期望的方式运行,那你就会遇到麻烦。所以,让我们了解一下this
这个敌人,以便学习如何对抗它:
什么是this
this
让我们先从在最佳情况下我们期望如何工作的基本知识开始:
'use strict';
class Person {
name;
constructor(theirName) {
this.name = theirName;
}
introduce() {
console.log("Hello I'm " + this.name);
}
}
const william = new Person("Bill");
william.introduce(); // Prints out "Hello I'm Bill"
这很简单:有一个名为 的对象类Person
。每个对象Person
都记住一个名为 的变量name
,并有一个名为 的方法introduce
。当你调用introduce
某个人时,它会查看该人的name
简介并打印出来。所以,是对我们正在查看的this
实例的对象的引用,对吗?introduce
嗯,不完全是。看看这个:
// Continued from above
// This doesn't RUN william's introduce function,
// it makes a REFERENCE to it
const introduceWilliam = william.introduce;
// Because it's a reference to a method that worked,
// we might assume the reference will also work but...
introduceWilliam();
// Uncaught TypeError! Cannot read property 'name' of undefined
现在,我们深入研究了平静的表面之下90年代编写的函数式编程语言的黑暗深处。
你必须记住,就 JavaScript 而言,函数只是另一种对象。它们可以在任何地方存储、传递和执行。
当你调用 时someThing.someFunc()
,JavaScript 会解析出你想在的上下文中执行 中的指令someFunc
someThing
。也就是说,设置this
为someThing
,然后执行指令。
但是如果你引用了someFunc
,你就可以在任何地方执行它。上面我们在全局上下文中调用了它,这在严格模式下会保留this
as undefined
。你甚至可以使用函数call
或apply
方法(函数上的函数!)来提供你想要的任何上下文和参数。
让我们编写一些有点可怕的代码来证明这一点:
// Still using william from above
const william = new Person("Bill");
// Make a reference to william's introduce method
let introduce = william.introduce;
// Make an unrelated object - Bagel the Beagle
const puppy = { name: "Bagel", breed: "Beagle" };
// Run function with manual `this` - Dogs can talk now
introduce.call(puppy); // Prints "Hello I'm Bagel"
驯服this
野兽
这非常强大,而且往往是不必要的。就像许多非常强大的东西一样,它也非常危险。由于我们经常传递函数引用(例如,this
用作button
s 或s 的回调), 的未绑定特性随时可能让你陷入困境。form
this
那么,我们该如何驯服this
?我可以挥舞着拐杖,嘶哑地对你说:“嗯,回到**我的*日子……*” 但事实是,JavaScript 的 ES5 和 ES2015 修订版为我们提供了限制游移this
值所需的一切:
函数.prototype.bind()
ES5 中添加的第一个工具是函数,这是2000 年代各种实用程序库所创新的黑客bind()
标准化。this
// Bind this reference to introduce so this is ALWAYS william.
let alwaysIntroduceWilliam = william.introduce.bind(william);
alwaysIntroduceWilliam(); // Prints "Hello I'm Bill"
alwaysIntroduceWilliam.call(puppy); // Prints "Hello I'm Bill"
bind
顾名思义,它的作用就是将函数绑定到一个选定的上下文中this
,确保其中的指令始终在我们选择的上下文中运行。在这里你可以看到,即使我们尝试使用call
来设置不同的this
, 也会bind
超过 ,并且我们总是会引入william
。这是修复 的重要第一步this
,但如今它不太常用,因为……
带箭头 =>
ES2015 新增了箭头函数,它(几乎是无意中)为我们提供了最常用的固定this
值获取方式。这是因为箭头函数在其定义上下文中创建了一个闭包。这意味着箭头函数内部引用的所有变量始终指向与箭头函数首次解析时相同的内存位置。
这对于捕获局部变量以便稍后使用非常有用,但它还有一个额外的好处,就是捕获this
箭头定义时设置的值。而且,由于this
(基本上)总是在构造过程中创建的对象,我们可以使用箭头函数来创建行为与this
我们预期完全一致的方法:
// Rewriting Person with arrows
class ArrowPerson {
name;
constructor(theirName) {
this.name = theirName;
}
introduce = () => {
// The arrow captures `this` so it is actually a
// reference to THIS Person.
console.log("Hello I'm " + this.name);
}
}
const arrowBill = new ArrowPerson("Arrow Bill");
arrowBill.introduce(); // "Hello I'm Arrow Bill"
// Now `this` is fixed even as we pass the function around:
const introduceRef = arrowBill.introduce;
introduceRef(); // "Hello I'm Arrow Bill"
introduceRef.call(puppy); // "Hello I'm Arrow Bill"
this
现在一切都更有意义了
我希望你this
现在能理解得更清楚一些。说实话,光是把这些都写出来,我就觉得理解得更透彻了。而且,由于 JavaScriptthis
会影响所有转译成 JavaScript 的代码,希望这也能帮助你理解 Typescript 等其他语言中函数上下文的复杂之处。
如果您对 有任何疑问this
,请在下方评论区留言。即使从事网络写作多年,我仍然在学习,所以我确信其中肯定存在一些可怕的危险,以及一些this
我忘记或还不知道的有趣事实。