原文:https://hackernoon.com/javascript-the-keyword-this-for-beginners-fb5238d99f85
理解 JavaScript 中的关键字 this
,以及它指向什么,有时候可能是有点复杂。幸好有五个通用规则可以用来确定 this
绑定到什么。虽然这些规则在某些情况下并不适用,不过绝大多数情况应该能帮你搞定。所以,下面我们开始吧!
this
的值通常是由一个函数的执行上下文决定的。所谓执行上下文就是指一个函数如何被调用。- 必须要知道的是,每次函数被调用时,
this
都有可能是不同的(即指向不同的东西)。 - 如果依然搞不清楚第一条和第二条的含义,没有关系,本文结尾的时候会搞清楚的。
#1 全局对象
好吧,我们现在把理论定义放一边,下面先从实践来寻找支撑。打开你的 Chrome 开发者控制台(Windows: Ctrl + Shift + J)(Mac: Cmd + Option + J),并键入如下代码:
1 | console.log(this); |
会得到什么呢?
1 | // Window |
window
对象!这是因为在全局作用域中,this
指向全局对象。而在浏览器中,全局对象就是 window
对象。
为帮助更好地理解为什么 this
指向 window
对象,下面我们更深入来了解一下。在控制台中,创建一个新变量 myName
,把它赋值为你的姓名:
1 | var myName = 'Brandon'; |
我们可以通过调用 myName
,再次访问这个新变量:
1 | myName |
不过,你知道在全局作用域中声明的每个变量都是绑定到 window
对象吗?好吧,我们来试一试:
1 | window.myName |
酷。所以,之前在全局上下文中执行 console.log(this)
时,我们知道 this
是在全局对象上被调用。因为浏览器中的全局对象是 window
对象,所以这样的结果是讲得通的:
1 | console.log(this) |
现在我们把 this
放在一个函数内。回忆我们之前的定义:this
的值通常是由函数如何被调用来决定的。记住这一点后,你期望这个函数返回什么?在浏览器控制台中,复制如下代码,按回车。
1 | function test() { |
关键字 this
再次返回全局对象(window)。这是因为 this
不在一个已声明的对象内,所以它默认等于全局对象(window)。这个概念可能现在理解起来有点难,不过随着进一步阅读,应该会越来越清晰。要指出的一件事是,如果你使用严格模式,那么上例中的 this
将会是 undefined
。
#2 已声明的对象
当关键字 this
被用在一个已声明的对象内时,this
的值被设置为被调用的方法所在的最近的父对象。看看如下代码,这里我声明了一个对象 person
,并在方法 full
内使用 this
:
1 | var person = { |
为更好阐述 this
实际上就是引用 person 对象,请将如下代码复制到浏览器控制台中。它与上面的代码大致相同,只是用 console.log(this)
替换了一下,这样我们就能看看会返回什么。
1 | var person = { |
如你所见,控制台会返回 person
对象,证明 this
采用的是 person
的值。
在继续之前,还有最后一件事。还记得我们说过 this
的值被设置为被调用的方法所在的最近的父对象吧?如果有嵌套的对象,你会期望发生什么?看看下面的示例代码。我们已经有了一个 person
对象,该对象像以前一样有同样的 first
、last
和 full
键。不过这一次,我们还嵌套进了一个 personTwo
对象。personTwo
包含了同样的三个键。
1 | var person = { |
当调用这两个 full
方法时,会发生什么呢?下面我们来探个究竟。
1 | person.full(); |
this
的值再次被设置为方法调用所在的最近的父对象。当调用 person.full()
时,函数内的 this
是被绑定到 person
对象。与此同时,当调用 person.personTwo.full()
时,在 full
函数内,this
是被绑定到 personTwo
对象!
#3 New
关键字
当使用 new
关键字(构造器)时,this
被绑定到正在新创建的对象。
我们来看一个示例:
1 | function Car(make, model) { |
上面,你可能会猜 this
被绑定到全局对象 - 如果不用关键字 new
的话,你就是正确的。当我们使用 new
时,this
的值被设置为一个空对象,在本例中是 myCar
。
1 | var myCar = new Car('Ford', 'Escape'); |
要把上面的代码搞清楚,需要理解 new
关键字到底做了什么。不过这本身是一个全新的话题。所以现在,如果你不确定的话,只要看到关键字 new
,就当 this
是正指向一个全新的空对象好了。
#4 Call、Bind 和 Apply
最后一个要点,我们实际上是可以用 call()
、bind()
和 apply()
显式设置 this
的值的。这三个方法很相似,不过理解它们之间的细微差别很重要。
call()
和 apply()
都是立即被调用的。call()
可以带任意数量的参数:this,后跟其它的参数。apply()
只带两个参数:this,后跟一个其它参数的数组。
还跟得上我的思路吧?用一个例子应该会解释得更清楚一些。看看下面的代码。我们正试着对数字求和。复制如下代码到浏览器控制台,然后调用该函数。
1 | function add(c, d) { |
add
函数输出的是 NaN
(不是一个数字)。这是因为 this.a
和 this.b
都是 undefined,二者都是不存在的。而我们不能把数字与未定义的东西相加。
下面我们给等式引入一个对象。我们可以用 call()
和 apply()
在我们的对象上调用 add
函数:
1 | function add(c, d) { |
当用 add.call()
时,第一个参数就是 this
要绑定的对象。后续的参数被传递进我们调用的函数。因此,在 add()
中,this.a
指向 ten.a
,this.b
指向 ten.b
,我们得到返回的 1+2+3+4
,或者 10。
add.apply()
差不多。第一个参数就是 this
应该绑定的对象。后续的参数是要用在函数中的参数数组。
那么 bind()
又是怎么样的呢?bind()
中的参数与 call()
中的是一样的,不过 bind()
不是马上被调用,而是返回一个 this
上下文已经绑定好的函数。因此,如果预先不知道所有参数的话,bind()
就很有用。下面再用一个示例来帮助理解:
1 | var small = { |
将上述代码复制到控制台。然后调用:
1 | small.go(2,3,4); |
很棒。这里并没有啥新东西。但是,如果我们想用 large.a
的值来替换,该怎么办呢?我们可以用 call()
或者 apply()
:
1 | small.go.call(large,2,3,4); |
现在,如果还不知道所有 3 个参数,该怎么办呢?我们可以用 bind()
:
1 | var bindTest = small.go.bind(large,2); |
如果在控制台输出上面的变量 bindTest
,就可以看到结果:
1 | console.log(bindTest); |
记住,用 bind()
的话,会返回一个已经绑定了 this
的函数。所以 this
已经成功绑定到 large
对象。我们还传递进了第二个参数 2
。之后,当知道其余参数时,我们可以将它们传递进来:
1 | bindTest(3,4); |
为清晰起见,我们将所有代码放在一个块中。仔细看一遍,然后将其复制到控制台中,真正了解一下发生了什么!
1 | var small = { |
#5 箭头函数
箭头函数本身就是一个很大的主题,为此我写了一整篇文章来讲解它:为初学者介绍箭头函数。
总结
你成功了!现在,在大多数情况下,你应该能推断出 this
指向什么!请记住如下几件事:
this
的值通常是由函数的执行上下文决定的- 在全局作用域中,
this
指向全局对象(window 对象) - 当使用
new
关键字(一个构造器)时,this
被绑定到正创建的新对象 - 可以显式用
call()
、bind()
和apply()
设置this
的值 - 箭头函数不会绑定
this
-this
是词法绑定的(即,基于原始上下文)