在许多面向对象语言中,this
是实例方法用来引用它们自身的一个变量。在所有语言中 this
通常都是一个不可变的引用或者指针,用来引用当前运行代码的对象、类或其他实体的关键字。因此,this
所引用的实体取决于执行上下文(例如,哪个对象正在调用它的方法)。
this 的作用
前面说到 this
是实例方法用来引用它们自身的一个变量,下面我们来说一下为什么要使用 this
。
比如我们有一个 say() 函数,用来输出一段字符串。
1 | function say(name) { |
我们传入了 name
参数,则在控制台输出了 I'm
字符串与 name
参数的组合。如果我们要将 say() 应用的更广一点:
1 | const person1 = { name: 'A' }; |
这时我们需要调用 say() 函数并手动传入 name
值。这时我们可以抽象一下 person1
和 person2
,让其复用 say() 函数。
1 | function say(name) { |
这里我们用闭包缓存了 name
,并在 greet() 函数中显式传入了 name
,也就是说 person1.greet()
相当于 say(person1.name)
,因此才能正确的调用 say() 函数。下面我们用 this
来实现一下这个功能。
1 | function say() { |
这里 say() 函数不再需要传入 name
参数,而是直接使用 this.name
。在 Person
构造器的中定义 Person
的 name
为传入的 name
参数,greet
为 say() 函数,这里直接使用了 say() 函数并且没有做任何更改。可以看到 person1
和 person2
成功的输出了自己的名字。
可以看到使用 this
来隐式地 “传递” 参数,相比于其他方式更加简洁也更好实现复用。
this 的指向
开头我们说过,this
所引用的实体取决于执行上下文(例如,哪个对象正在调用它的方法)。
在 JavaScript 中以下几种情况:
函数上下文
简单调用
1 | // 在 Node 中 |
上例中在 f1()
函数中嵌套了 f2()
函数,f2()
函数中嵌套了 f3()
函数。并且都是直接调用了对应的函数,可以看到此时 this
的指向均为 Node 中的全局对象 global
。在浏览器中全局对象为 window
。也就是说使用简单调用可以用 this
来调用全局对象的属性。
1 | // 在浏览器中 |
作为对象的方法
1 | const person1 = { |
当函数作为对象里的方法被调用时,它们的 this
是调用该函数的对象。上例中 greet() 函数的 this
为 person1
。
作为构造函数
1 | function Person(name) { |
当一个函数用作构造函数时(使用 new 关键字),它的 this
被绑定到正在构造的新对象。上例中 Person 构造函数中的 this
指向了 Person,因此赋值语句含义为:为 Person 的 name
属性赋值为传入参数的 name
。
需要注意的是如果显式返回了一个对象,那么与 this
绑定的默认对象将被丢弃,不显式返回则没有这个问题。
1 | function Person(name) { |
Function.prototype 的 call、apply 或 bind 调用
1 | const person1 = { |
当一个函数在其主体中使用 this
关键字时,可以通过使用函数继承自 Function.prototype 的 call
或 apply
方法将 this
值绑定到调用中的特定对象。上例中第 9 行将 this
绑定到了对象 { name: 'B' }
上,而第 10 行将 this
绑定到了对象 { name: 'C' }
上。
1 | const person1 = { |
ECMAScript 5 引入了 Function.prototype.bind 方法。调用 function.bind(someObject) 会创建一个与 function 具有相同函数体和作用域的函数,但是在这个新函数中,this
将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。可以看到 person2Greet
将 this
绑定到对象 { name: 'B' }
不可再绑定到其他对象。
原型链中的 this
1 | function Person() {} |
JavaScript 给对象提供了一个 __proto__
的属性,指向该对象构造器的原型对象。上例中,对象 person1
的 greet
属性继承自它的原型 Person
。在调用 greet
时,首先遍历 person1
中的所有属性,没有找到 greet
于是接着查找 person1
构造器的原型,然后在 Person
中找到了 greet
属性。查找过程是从 person1.greet
的引用开始,所以函数中的 this
指向 person1
,也就是说,因为 greet
是作为 person1
的方法调用的,所以它的 this
指向了 person1
,于是 this.name
为 A
。
getter 与 setter 中的 this
1 | class Person { |
与原型链中的 this
类似,用作 getter
或 setter
的函数都会把 this
绑定到设置或获取属性的对象。
全局上下文
无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this
都指代全局对象。
1 | // 在浏览器中 |
在浏览器中 window
对象同时也是全局对象,因此 this
指代 window
。但是在 Node 环境有所不同:
1 | // 在 Node 中 |
在 Node 中 this
指代 module.exports
,这是因为在 Node 执行这段代码时,Node 会将这段代码包裹在一个 wrapper
函数中再导出。
1 | const moduleMock = { exports: {} }; |
这里相当于 Function.prototype 的 apply 调用,因此 this
指向 module.exports
。上例中指向了 moduleMock.exports
。
补充
箭头函数
1 | // 在浏览器中 |
在箭头函数中,this
与封闭词法上下文的 this
保持一致。在全局代码中,它将被设置为全局对象。上例中 f1() 函数在浏览器环境 this
指向了全局对象 window
,但是在 Node 环境中 this
指向了 module.exports
,原因在全局上下文已经解释过。而在构造函数 Person 中,this
指向了 Person ,因此 greet() 函数输出了 person1
的 name
。
注意:如果将 this 传递给 call、apply、或者 bind,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为 null。(也就是说无法通过 call、apply 或者 bind 来重置 this 的指向)
this 的丢失
开头说过:this
所引用的实体取决于执行上下文(例如,哪个对象正在调用它的方法)。
1 | const person1 = { |
在 person1
中调用 greet() 此时作为对象的方法调用,因此 this
指向了 person1
。但是当使用 greetCopy 引用 person1
中的 greet() 时,此时是简单调用的方式,this
指向了全局对象。另外在回调函数中也会出现这种情况:
1 | function say() { |
我们使用 callback() 函数传递了 person1.greet
函数,此时相当于进行了隐式赋值,与上一种情况的 const greetCopy = person1.greet
类似,这里 fn = person1.greet
,所以在 callback() 中 this
也就指向了全局对象。
this 指向的判断
在了解了上面不同 this
指向的调用方法后,现在已经很好判断 this
的指向了,但是如果有复合的情况 this
又会指向什么呢。
从调用方式来看可以简单的分为下面 4 种:
- 简单调用(直接调用使用了
this
的函数) - 依托于上下文调用(不管是作为对象的方法、原型链中的 this、getter 与 setter 还是箭头函数,都是在某个对象下再调用使用了
this
的函数) - 作为构造函数调用(使用了 new,
this
指向绑定的新对象) - Function.prototype 的 call、apply 或 bind 调用(使用 Funnction.prototype 的方法改变了
this
的绑定)
一般来说简单调用不依托于其他上下文,此时 this
指向全局对象,因此简单对象的优先级最低。在 Function.prototype 的 call、apply 或 bind 调用的介绍中,我们使用 call、apply 或 bind 改变了作为对象的方法调用中的 this
,因此 Function.prototype 的 call、apply 或 bind 调用的优先级要高于依托于上下文的调用。
此时优先级排序为:方法 4 > 方法 2 > 方法 1 。接下来我们看一下作为构造函数的优先级如何。
1 | function setName(name) { |
可以看到 person2
作为构造函数调用后, person2.name
值为 C
,而此时 person1.name
值仍然为 B
,说明作为构造函数调用把原 person1.setName()
中 this
指向 person1
更改为了新的对象 person2
,故 person2.name
值为传入的 C
。
此时的优先级排序为:方法 3 > 方法 2 > 方法 1 。由于 new 无法与 call / apply 一起使用,我们只能使用 bind ,因为 bind 会创建一个新的构造函数。
1 | function setName(name) { |
同样地 person2
作为构造函数调用后,person2.name
的值为 B
,而 person1.name
的值仍然为 A
,说明作为构造函数调用把原 person1SetName
中 this
指向 person1
更改为了新的对象 person2
,故 person2.name
值为传入的 B
。
使用 new 来更改 bind 的绑定,可以实现预先设置函数的一些参数,这样在使用时只用传入剩余参数,也就是柯里化。
1 | function add(x, y) { |
上例中 bind 不再关心绑定对象,因为 new 会重置绑定对象,所以传入了 null 。可以看到基于 add() 函数实现了 addOne() 和 addTwo() 功能的函数。
所以作为构造函数调用的优先级应该高于 Function.prototype 的 call、apply 或 bind 调用,所以方法 3 优先级高于 方法 4。此时的优先级排序为:方法 3 > 方法 4 > 方法 2 > 方法 1 。
总结
this 的指向可以按照下面的顺序来进行判断:
- 是否作为构造函数调用(new)?如果是则
this
指向新创建的对象。 - 是否使用 Function.prototype 的 call、apply 或 bind 调用?如果是则
this
指向绑定的对象。 - 是否依托于上下文对象调用?如果是则
this
指向具体的上下文对象。 - 否则是简单调用,
this
指向全局对象。
参考资料: