by Lukas Gisder-Dubé
卢卡斯·吉斯杜比(LukasGisder-Dubé)
如何在JavaScript中理解关键字this和context (How to understand the keyword this and context in JavaScript)
As mentioned in one of my earlier articles, mastering JavaScript fully can be a lengthy journey. You may have come across this
on your journey as a JavaScript Developer. When I started out, I first saw it when using eventListeners
and with jQuery. Later on, I had to use it often with React and I am sure you also did. That does not mean that I really understood what it is and how to fully take control of it.
正如我之前的一篇文章中提到的那样 ,完全掌握JavaScript可能是一段漫长的旅程。 作为JavaScript开发人员,您可能在旅途中遇到了this
问题。 当我开始使用jQuery和eventListeners
时,我第一次看到它。 后来,我不得不经常将它与React一起使用,并且我相信您也这样做了。 这并不意味着我真的了解了它是什么以及如何完全控制它。
However, it is very useful to master the concept behind it, and when approached with a clear mind, it is not very difficult either.
但是,掌握其背后的概念非常有用,而且当头脑清晰时,也不是很困难。
挖这个 (Digging into this)
Explaining
this
can lead to a lot of confusion, simply by the naming of the keyword.仅仅通过关键字的命名来解释
this
会导致很多混乱。
this
is tightly coupled to what context you are in, in your program. Let’s start all the way at the top. In our browser, if you just type this
in the console, you will get the window
-object, the outermost context for your JavaScript. In Node.js, if we do:
this
与程序中所处的上下文紧密相关。 让我们从头开始。 在我们的浏览器中,如果仅在控制台中键入this
,则将获得window
-object,即JavaScript的最外层上下文。 在Node.js中,如果这样做:
console.log(this)
we end up with {}
, an empty object. This is a bit weird, but it seems like Node.js behaves that way. If you do
我们最后得到一个空对象{}
。 这有点怪异,但似乎Node.js的行为是这样的。 如果你这样做
(function() {
console.log(this);
})();
however, you will receive the global
object, the outermost context. In that context setTimeout
, setInterval
, are stored. Feel free to play around a little bit with it to see what you can do with it. As from here, there is almost no difference between Node.js and the browser. I will be using window
. Just remember that in Node.js it will be the global
object, but it does not really make a difference.
但是,您将收到global
对象,即最外部的上下文。 在该上下文中,存储setTimeout
, setInterval
。 随意尝试一下它,看看您可以使用它做什么。 从这里开始,Node.js和浏览器之间几乎没有区别。 我将使用window
。 请记住,在Node.js中它将是global
对象,但实际上并没有什么不同。
Remember: Context only makes sense inside of functions
切记:上下文仅在函数内部有意义
Imagine you write a program without nesting anything in functions. You would simply write one line after another, without going down specific structures. That means you do not have to keep track of where you are. You are always on the same level.
假设您在编写程序时没有在函数中嵌套任何内容。 您只需要逐行写一行,而无需了解特定的结构。 这意味着您不必跟踪自己的位置。 您始终处于同一级别。
When you start having functions, you might have different levels of your program and this
represents where you are, what object called the function.
当您开始拥有函数时,您可能具有不同级别的程序,并且this
表示您在哪里,该对象称为函数。
跟踪调用者对象 (Keeping track of the caller object)
Let’s have a look at the following example and see how this
changes depending on the context:
让我们来看看下面的例子,看看this
取决于环境的变化:
const coffee = {
strong: true,
info: function() {
console.log(`The coffee is ${this.strong ? '' : 'not '}strong`)
},
}
coffee.info() // The coffee is strong
Since we call a function that is declared inside the coffee
object, our context changes to exactly that object. We can now access all of the properties of that object through this
. In our example above, we could also just reference it directly by doing coffee.strong
. It gets more interesting, when we do not know what context, what object, we are in or when things simply get a bit more complex. Have a look at the following example:
由于我们调用了在coffee
对象内部声明的函数,因此我们的上下文将完全更改为该对象。 现在,我们可以通过this
访问该对象的所有属性。 在上面的示例中,我们还可以通过执行coffee.strong
直接引用它。 当我们不知道我们所处的上下文,对象是什么或事情变得更加复杂时,它会变得更加有趣。 看下面的例子:
const drinks = [
{
name: 'Coffee',
addictive: true,
info: function() {
console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`)
},
},
{
name: 'Celery Juice',
addictive: false,
info: function() {
console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`)
},
},
]
function pickRandom(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
pickRandom(drinks).info()
类和实例 (Classes and Instances)
Classes can be used to abstract your code and share behavior. Always repeating the info
function declaration in the last example is not good. Since classes and their instances are in fact objects, they behave in the same way. One thing to note however, is that declaring this
in the constructor actually is a prediction for the future, when there will be an instance.
类可用于抽象代码并共享行为。 在最后一个示例中总是重复info
函数声明是不好的。 由于类及其实例实际上是对象,因此它们的行为方式相同。 有一件事然而需要注意的是,宣称this
在构造函数实际上是对未来的时候都会有一个实例的预测。
Let’s take a look:
让我们来看看:
class Coffee {
constructor(strong) {
this.strong = !!strong
}
info() {
console.log(`This coffee is ${this.strong ? '' : 'not '}strong`)
}
}
const strongCoffee = new Coffee(true)
const normalCoffee = new Coffee(false)
strongCoffee.info() // This coffee is strong
normalCoffee.info() // This coffee is not strong
陷阱:无缝嵌套的函数调用 (Pitfall: seamlessly nested function calls)
Sometimes, we end up in a context that we did not really expect. This can happen, when we unknowingly call the function inside another object context. A very common example is when using setTimeout
or setInterval
:
有时,我们最终遇到了一个我们没有真正期望的环境。 当我们在不知不觉中在另一个对象上下文中调用该函数时,可能会发生这种情况。 一个非常常见的示例是使用setTimeout
或setInterval
:
// BAD EXAMPLE
const coffee = {
strong: true,
amount: 120,
drink: function() {
setTimeout(function() {
if (this.amount) this.amount -= 10
}, 10)
},
}
coffee.drink()
What do you think coffee.amount
is?
您觉得coffee.amount
是多少?
...
...
..
..
.
。
It is still 120
. First, we were inside the coffee
object, since the drink
method is declared inside of it. We just did setTimeout
and nothing else. That’s exactly it.
现在仍然是120
。 首先,由于在对象内部声明了drink
方法,因此我们位于coffee
对象内部。 我们只是做了setTimeout
而已。 就是这样
As I explained earlier, the setTimeout
method is actually declared in the window
object. When calling it, we actually switch context to the window
again. That means that our instructions actually tried to change window.amount
, but it ended up doing nothing because of the if
-statement. To take care of that, we have to bind
our functions (see below).
如前所述, setTimeout
方法实际上是在window
对象中声明的。 调用它时,我们实际上再次将上下文切换到window
。 这意味着我们的指令实际上试图更改window.amount
,但是由于if
-statement而最终无济于事。 为了解决这个问题,我们必须bind
我们的功能(见下文)。
React (React)
Using React, this will hopefully be a thing of the past soon, thanks to Hooks. At the moment, we still have to bind
everything (more on that later) in one way or another. When I started out, I had no idea why I was doing it, but at this point, you should already know why it is necessary.
借助Hooks,使用React有望很快成为过去。 目前,我们仍然必须以一种或另一种方式bind
所有内容(稍后再介绍)。 当我刚开始的时候,我不知道为什么要这么做,但是在这一点上,您应该已经知道为什么这样做是必要的。
Let’s have a look at two simple React class Components:
让我们看一下两个简单的React类组件:
// BAD EXAMPLE
import React from 'react'
class Child extends React.Component {
render() {
return <button onClick = {
this.props.getCoffee
} > Get some Coffee! < /button>
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
coffeeCount: 0,
}
// change to turn into good example – normally we would do:
// this._getCoffee = this._getCoffee.bind(this)
}
render() {
return ( <
React.Fragment >
<
Child getCoffee = {
this._getCoffee
}
/> < /
React.Fragment >
)
}
_getCoffee() {
this.setState({
coffeeCount: this.state.coffeeCount + 1,
})
}
}
When we now click on the button rendered by the Child
, we will receive an error. Why? Because React changed our context when calling the _getCoffee
method.
现在,当我们单击由Child
渲染的按钮时,我们将收到一个错误。 为什么? 因为React在调用_getCoffee
方法时更改了上下文。
I assume that React does call the render method of our Components in another context, through helper classes or similar (even though I would have to dig deeper to find out for sure). Therefore, this.state
is undefined and we’re trying to access this.state.coffeeCount
. You should receive something like Cannot read property coffeeCount of undefined
.
我假设React确实通过辅助类或类似的东西在另一个上下文中调用了我们组件的render方法(即使我必须更深入地找出来才能确定)。 因此, this.state
是未定义的,我们正在尝试访问this.state.coffeeCount
。 您应该收到类似Cannot read property coffeeCount of undefined
。
To solve the issue, you have to bind
(we’ll get there) the methods in our classes, as soon as we pass them out of the component where they are defined.
要解决此问题,您必须在bind
类中的方法传递到定义它们的组件之外后,将它们bind
(到达那里)。
Let’s have a look at one more generic example:
让我们看一个更通用的示例:
// BAD EXAMPLE
class Viking {
constructor(name) {
this.name = name
}
prepareForBattle(increaseCount) {
console.log(`I am ${this.name}! Let's go fighting!`)
increaseCount()
}
}
class Battle {
constructor(vikings) {
this.vikings = vikings
this.preparedVikingsCount = 0
this.vikings.forEach(viking => {
viking.prepareForBattle(this.increaseCount)
})
}
increaseCount() {
this.preparedVikingsCount++
console.log(`${this.preparedVikingsCount} vikings are now ready to fight!`)
}
}
const vikingOne = new Viking('Olaf')
const vikingTwo = new Viking('Odin')
new Battle([vikingOne, vikingTwo])
We’re passing the increaseCount
from one class to another. When we call the increaseCount
method in Viking
, we have already changed context and this
actually points to the Viking
, meaning that our increaseCount
method will not work as expected.
我们将increaseCount
从一个类传递到另一个类。 当我们调用increaseCount
方法Viking
,我们已经改变了背景和this
实际上指向Viking
,这意味着我们的increaseCount
方法不会按预期方式工作。
解决方案—绑定 (Solution — bind)
The simplest solution for us is to bind
the methods that will be passed out of our original object or class. There are different ways where you can bind functions, but the most common one (also in React) is to bind it in the constructor. So we would have to add this line in the Battle
constructor before line 18:
对我们而言,最简单的解决方案是bind
将从原始对象或类中传递出去的方法。 绑定函数有多种方法,但是最常见的一种方法(也在React中)是在构造函数中进行绑定。 因此,我们必须在第18行之前的Battle
构造函数中添加此行:
this.increaseCount = this.increaseCount.bind(this)
You can bind any function to any context. This does not mean that you always have to bind the function to the context it is declared in (this is the most common case, however). Instead, you could bind it to another context. With bind
, you always set the context for a function declaration. This means that all calls for that function will receive the bound context as this
. There are two other helpers for setting the context.
您可以将任何功能绑定到任何上下文。 这并不意味着您总是必须将函数绑定到声明它的上下文中(不过,这是最常见的情况)。 相反,您可以将其绑定到另一个上下文。 使用bind
,您总是可以为函数声明设置上下文 。 这意味着,该函数的所有调用将获得绑定的上下文this
。 还有另外两个用于设置上下文的助手。
Arrow functions `() => {}` automatically bind the function to the declaration context
箭头函数`()=> {}`自动将函数绑定到声明上下文
申请并致电 (Apply and call)
They both do basically the same thing, just that the syntax is different. For both, you pass the context as first argument. apply
takes an array for the other arguments, with call
you can just separate other arguments by comma. Now what do they do? Both of these methods set the context for one specific function call. When calling the function without call
, the context is set to the default context (or even a bound context). Here is an example:
它们的基本作用相同,只是语法不同。 对于这两种情况,您都将上下文作为第一个参数传递。 apply
将其他参数作为一个数组,使用call
可以用逗号分隔其他参数。 现在他们做什么? 这两种方法都为一个特定的函数调用设置了上下文。 在不call
的情况下调用函数时,上下文设置为默认上下文(甚至是绑定上下文)。 这是一个例子:
class Salad {
constructor(type) {
this.type = type
}
}
function showType() {
console.log(`The context's type is ${this.type}`)
}
const fruitSalad = new Salad('fruit')
const greekSalad = new Salad('greek')
showType.call(fruitSalad) // The context's type is fruit
showType.call(greekSalad) // The context's type is greek
showType() // The context's type is undefined
Can you guess what the context of the last showType()
call is?
您能猜出最后一个showType()
调用的上下文是什么吗?
…
…
..
..
.
。
You’re right, it is the outermost scope, window
. Therefore, type
is undefined
, there is no window.type
没错,它是最外面的作用域window
。 因此, type
是undefined
,没有window.type
This is it, hopefully you now have a clear understanding on how to use this
in JavaScript. Feel free to leave suggestions for the next article in the comments.
这是它,希望你现在有关于如何使用一个清晰的认识this
在JavaScript中。 随时在评论中留下有关下一篇文章的建议。
About the Author: Lukas Gisder-Dubé co-founded and led a startup as CTO for 1 1/2 years, building the tech team and architecture. After leaving the startup, he taught coding as Lead Instructor at Ironhack and is now building a Startup Agency & Consultancy in Berlin. Check out dube.io to learn more.
关于作者:LukasGisder-Dubé与他人共同创立并领导了一家初创公司担任CTO长达1 1/2年,建立了技术团队和架构。 离开创业公司后,他在Ironhack担任首席讲师的编码课程,现在正在柏林建立创业公司和咨询公司。 查看dube.io了解更多信息。