理解 JavaScript 中的关键字 this,以及它指向什么,有时候可能是有点复杂。幸好有五个通用规则可以用来确定 this 绑定到什么。更多互联网资讯请去公众号锁定"全阶魔方"每天都有更新哦虽然这些规则在某些情况下并不适用,不过绝大多数情况应该能帮你搞定。所以,下面我们开始吧!
1. this 的值通常是由一个函数的执行上下文决定的。所谓执行上下文就是指一个函数如何被调用。
2. 必须要知道的是,每次函数被调用时,this 都有可能是不同的(即指向不同的东西)。
3. 如果依然搞不清楚第一条和第二条的含义,没有关系,本文结尾的时候会搞清楚的。
#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
2. // returns -> 'Brandon'
不过,你知道在全局作用域中声明的每个变量都是绑定到 window 对象吗?好吧,我们来试一试:
1. window.myName
2. // returns -> 'Brandon'
3.
4. window.myName === myName
5. // returns -> true
酷。所以,之前在全局上下文中执行 console.log(this) 时,我们知道 this 是在全局对象上被调用。因为浏览器中的全局对象是 window对象,所以这样的结果是讲得通的:
1. console.log(this)
2. // returns -> window{...}
现在我们把 this 放在一个函数内。回忆我们之前的定义:this 的值通常是由函数如何被调用来决定的。记住这一点后,你期望这个函数返回什么?在浏览器控制台中,复制如下代码,按回车。
1. function test() {
2. return this;
3. }
4. test()
关键字 this 再次返回全局对象(window)。这是因为 this 不在一个已声明的对象内,所以它默认等于全局对象(window)。这个概念可能现在理解起来有点难,不过随着进一步阅读,应该会越来越清晰。要指出的一件事是,如果你使用严格模式,那么上例中的 this 将会是 undefined。
当关键字 this 被用在一个已声明的对象内时,this 的值被设置为被调用的方法所在的最近的父对象。看看如下代码,这里我声明了一个对象 person,并在方法 full 内使用 this:
1. var person = {
2. first: 'John',
3. last: 'Smith',
4. full: function() {
5. console.log(this.first + ' ' + this.last);
6. }
7. };
8.
9. person.full();
10. // logs => 'John Smith'
为更好阐述 this 实际上就是引用 person 对象,请将如下代码复制到浏览器控制台中。它与上面的代码大致相同,只是用 console.log(this) 替换了一下,这样我们就能看看会返回什么。
1. var person = {
2. first: 'John',
3. last: 'Smith',
4. full: function() {
5. console.log(this);
6. }
7. };
8.
9. person.full();
10. // logs => Object {first: "John", last: "Smith", full: function}
如你所见,控制台会返回 person 对象,证明 this 采用的是 person 的值。
在继续之前,还有最后一件事。还记得我们说过 this 的值被设置为被调用的方法所在的
最近的父对象吧?如果有嵌套的对象,你会期望发生什么?看看下面的示例代码。我们已经有了一个 person 对象,该对象像以前一样有同样的 first、last 和 full 键。不过这一次,我们还嵌套进了一个 personTwo 对象。personTwo 包含了同样的三个键。1. var person = {
2. first: 'John',
3. last: 'Smith',
4. full: function() {
5. console.log(this.first + ' ' + this.last);
6. },
7. personTwo: {
8. first: 'Allison',
9. last: 'Jones',
10. full: function() {
11. console.log(this.first + ' ' + this.last);
12. }
13. }
14. };
当调用这两个 full 方法时,会发生什么呢?下面我们来探个究竟。
1. person.full();
2. // logs => 'John Smith'
3.
4. person.personTwo.full();
5. // logs => 'Allison Jones'
this 的值再次被设置为方法调用所在的最近的父对象。当调用 person.full() 时,函数内的 this 是被绑定到 person 对象。与此同时,当调用 person.personTwo.full() 时,在 full 函数内,this 是被绑定到 personTwo 对象!
#3 New 关键字
当使用 new 关键字(构造器)时,this 被绑定到正在新创建的对象。
我们来看一个示例:
1. function Car(make, model) {
2. this.make = make;
3. this.model = model;
4. };
上面,你可能会猜 this 被绑定到全局对象 - 如果不用关键字 new 的话,你就是正确的。当我们使用 new 时,this 的值被设置为一个空对象,在本例中是 myCar。
1. var myCar = new Car('Ford', 'Escape');
2.
3. console.log(myCar);
4. // logs => Car {make: "Ford", model: "Escape"}
要把上面的代码搞清楚,需要理解 new 关键字到底做了什么。不过这本身是一个全新的话题。所以现在,如果你不确定的话,只要看到关键字 new,就当 this 是正指向一个全新的空对象好了。
#4 Call、Bind 和 Apply
最后一个要点,我们实际上是可以用 call()、bind() 和 apply() 显式设置 this 的值的。这三个方法很相似,不过理解它们之间的细微差别很重要。
call() 和 apply() 都是立即被调用的。call() 可以带任意数量的参数:this,后跟其它的参数。apply() 只带两个参数:this,后跟一个其它参数的数组。
还跟得上我的思路吧?用一个例子应该会解释得更清楚一些。看看下面的代码。我们正试着对数字求和。复制如下代码到浏览器控制台,然后调用该函数。
1. function add(c, d) {
2. console.log(this.a + this.b + c + d);
3. }
4.
5. add(3,4);
6. // logs => NaN
add 函数输出的是 NaN(不是一个数字)。这是因为 this.a 和 this.b 都是 undefined,二者都是不存在的。而我们不能把数字与未定义的东西相加。
下面我们给等式引入一个对象。我们可以用 call() 和 apply() 在我们的对象上调用 add 函数:
1. function add(c, d) {
2. console.log(this.a + this.b + c + d);
3. }
4.
5. var ten = {a: 1, b: 2};
6.
7. add.call(ten, 3, 4);
8. // logs => 10
9.
10. add.apply(ten, [3,4]);
11. // logs => 10
当用 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 = {
2. a: 1,
3. go: function(b,c,d){
4. console.log(this.a+b+c+d);
5. }
6. }
7.
8. var large = {
9. a: 100
10. }
将上述代码复制到控制台。然后调用:
1. small.go(2,3,4);
2. // logs 1+2+3+4 => 10
很棒。这里并没有啥新东西。但是,如果我们想用 large.a 的值来替换,该怎么办呢?我们可以用 call() 或者 apply():
1. small.go.call(large,2,3,4);
2. // logs 100+2+3+4 => 109
现在,如果还不知道所有 3 个参数,该怎么办呢?我们可以用 bind():
1. var bindTest = small.go.bind(large,2);
如果在控制台输出上面的变量 bindTest,就可以看到结果:
1. console.log(bindTest);
2. // logs => function (b,c,d){console.log(this.a+b+c+d);}
记住,用 bind() 的话,会返回一个已经绑定了 this 的函数。所以 this 已经成功绑定到 large 对象。我们还传递进了第二个参数 2。之后,当知道其余参数时,我们可以将它们传递进来:
1. bindTest(3,4);
2. // logs 100+2+3+4 => 109
为清晰起见,我们将所有代码放在一个块中。仔细看一遍,然后将其复制到控制台中,真正了解一下发生了什么!
1. var small = {
2. a: 1,
3. go: function(b,c,d){
4. console.log(this.a+b+c+d);
5. }
6. }
7.
8. var large = {
9. a: 100
10. }
11.
12. small.go(2,3,4);
13. // logs 1+2+3+4 => 10
14.
15. var bindTest = small.go.bind(large,2);
16.
17. console.log(bindTest);
18. // logs => function (b,c,d){console.log(this.a+b+c+d);}
19.
20. bindTest(3,4);
21. // logs 100+2+3+4 => 109
#5 箭头函数
箭头函数本身就是一个很大的主题,为此我写了一整篇文章来讲解它:。
总结
你成功了!现在,在大多数情况下,你应该能推断出 this 指向什么!请记住如下几件事:
1. this 的值通常是由函数的执行上下文决定的
2. 在全局作用域中,this 指向全局对象(window 对象)
3. 当使用 new 关键字(一个构造器)时,this 被绑定到正创建的新对象
4. 可以显式用 call()、bind() 和 apply() 设置 this 的值
5. 箭头函数不会绑定 this - this是词法绑定的(即,基于原始上下文)
閱讀更多 全階魔方 的文章