与其他语言相比,函数的 this
关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。
在绝大多数情况下,函数的调用方式决定了
this
的值(运行时绑定)。this
不能在执行期间被赋值,并且在每次函数被调用时this
的值也可能会不同。ES5 引入了bind
方法来设置函数的this
值,而不用考虑函数如何被调用的。ES2015 引入了箭头函数,箭头函数不提供自身的this
绑定(this
的值将保持为闭合词法上下文的值)。
涵义
当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
this
可以用在构造函数之中,表示实例对象。除此之外,this
还可以用在别的场合。但不管是什么场合,this
都有一个共同点:它总是返回一个对象。简单说,this
就是属性或方法“当前”所在的对象。
const p = {
name: 'jay',
describe() {
console.log(`${this.name} is a good man`)
}
}
p.describe() // jay is a good man
上面代码中,this.name
表示 name
属性所在的那个对象。由于 this.name
是在 describe
方法中调用,而 describe
方法所在的当前对象是 p
,因此 this
指向 p
,this.name
就是 p.name
。
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即 this
的指向是可变的。
const n = {
name: 'jj'
}
n.describe = p.describe
n.describe() // jj is a good man
上面代码中,p.describe
属性被赋给 n
,于是 n.describe
就表示 describe
方法所在的当前对象是 n
,所以this.name
就指向 n.name
。
关于 this
的指向改变的理解可以参考之前编译执行的相关文章,这里不做赘述。稍稍重构这个例子,this
的动态指向就能看得更清楚。
function describe () {
console.log(`${this.name} is a good man`)
}
const p = {
name: 'jay',
describe
}
const n = {
name: 'jj',
describe
}
p.describe() // jay is a good man
n.describe() // jj is a good man
只要函数被赋给另一个变量,this
的指向就会变。
const p = {
name: 'jay',
describe: function () {
console.log(`${this.name} is a good man`)
}
}
const name = 'jj'
const f = p.describe
f() // jj is a good man
上面代码中,p.describe
被赋值给变量 f
,内部的 this
就会指向 f
运行时所在的对象(本例是顶层对象)。
实质
JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this
就是函数运行时所在的对象(环境)。JavaScript 支持运行环境动态切换,也就是说,this
的指向是动态的,没有办法事先确定到底指向哪个对象。
const o = { n: 1 }
JavaScript 引擎会先在内存里面,生成一个对象 { n: 1 }
,然后把这个对象的内存地址赋值给变量 o
。也就是说,变量 o
是一个地址引用(reference)。后面如果要读取 o.n
,引擎先从 o
拿到内存地址,然后再从该地址读出原始的对象,返回它的n
属性。原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。
{
n: {
[[value]]: 1
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
属性的值可能是一个函数。
const o = { n: function () {} }
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给n
属性的 value
属性。
{
n: {
[[value]]: 函数的地址
...
}
}
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
const f = function () {}
const o = { f }
f() // 在全局对象执行
obj.f() // 在 obj 对象里面执行
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
使用场合
this
主要有以下几个使用场合。
全局上下文
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this
都指向全局对象。在浏览器环境下,它指的就是顶层对象 window
。可以使用 globalThis
获取全局对象,无论你的代码是否在当前上下文运行。
console.log(this === window) // true
函数上下文
在函数内部,this
的值取决于函数被调用的方式。
function f () {
return this
}
// 在浏览器中,全局对象是 window
console.log(f() === window) // true
//在 Node 中
console.log(f() === globalThis) // true
然而,在严格模式下,如果进入执行环境时没有设置 this
的值,this
会保持为 undefined
function f () {
"use strict"
return this
}
console.log(f() === undefined) // true
箭头函数
在箭头函数中,this
与封闭词法环境的 this
保持一致。在全局代码中,它将被设置为全局对象。
const f = () => this
console.log(f() === this) // true
构造函数
当一个函数用作构造函数时(使用new关键字),它的 this
被绑定到正在构造的新对象。
function C() {
this.a = 37
}
const o = new C()
console.log(o.a) // 37
DOM 事件处理函数
当函数被用作事件处理函数时,它的 this
指向触发事件的元素(一些浏览器在使用非 addEventListener
的函数动态地添加监听函数时不遵守这个约定)。
function bluify (e) {
console.log(this === e.currentTarget) // true
}
const element = document.getElementById('do')
element.addEventListener('click', bluify, false)
内联事件处理函数
当代码被内联 on-event
处理函数调用时,它的 this
指向监听器所在的 DOM 元素。
<button onclick="alert(this.tagName.toLowerCase());">Show this</button>
类上下文
this
在 class
中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。在类的构造函数中,this
是一个常规对象。类中所有非静态的方法都会被添加到 this
的原型中。
class P {
constructor () {
const proto = Object.getPrototypeOf(this)
console.log(Object.getOwnPropertyNames(proto))
}
first () {}
second () {}
static third () {}
}
new P() // [ 'constructor', 'first', 'second' ]
不像基类的构造函数,派生类的构造函数没有初始的 this
绑定。派生类不能在调用 super()
之前返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。
class A extends P { }
class B extends P {
constructor() {
return { a: 5 }
}
}
class C extends P {
constructor() {
super()
}
}
class D extends P {
constructor() { }
}
new A() // ['constructor']
new B() //
new C() // ['constructor']
new D() // ReferenceError: Must call super constructor in derived class before
对象的方法
当函数作为对象里的方法被调用时,this
被设置为调用该函数的对象。
const o = {
prop: 37,
f: function () {
return this.prop
}
}
console.log(o.f()) // 37
请注意,这样的行为完全不会受函数定义方式或位置的影响。如果对象的方法里面包含 this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
const o = { prop: 37 }
function independent() {
return this.prop
}
o.f = independent
console.log(o.f()) // 37
this
的绑定只受最接近的成员引用的影响,如果 this
所在的方法不在对象的第一层,这时 this
只是指向当前一层的对象,而不会继承更上面的层。
a = {
p: 1,
b: {
p: 2,
m: function() {
console.log(this, this.p)
}
}
}
a.b.m() // { p: 2, m: [Function: m] }, 2
但是,下面这几种用法,都会改变 this
的指向。
var o ={
n: function () {
console.log(this)
}
};
o.n()
// 情况一
(o.n = o.n)() // window
// => (o.n = function () { console.log(this) })()
// => (function () { console.log(this) })()
// 情况二
(false || o.n)() // window
// => (false || function () { console.log(this) })()
// 情况三
(1, o.n)() // window
// => (1, function () { console.log(this) })()
原型链
对于在对象原型链上某处定义的方法,同样的概念也适用。如果该方法存在于一个对象的原型链上,那么 this
指向的是调用这个方法的对象,就像该方法就在这个对象上一样。
const o = {
f: function () {
return this.a + this.b
}
}
const p = Object.create(o)
p.a = 1
p.b = 4
console.log(p.f()) // 5
getter 与 setter
再次,相同的概念也适用于当函数在一个 getter
或者 setter
中被调用。用作 getter
或 setter
的函数都会把 this
绑定到设置或获取属性的对象。
function sum() {
return this.a + this.b + this.c
}
const o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3
}
}
Object.defineProperty(o, 'sum', {
get: sum,
enumerable: true,
configurable: true
})
console.log(o.average, o.sum) // 2, 6
使用注意项
this
的动态切换,为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。所以在使用的时候,需要特别注意以下几点。
注意多层this
由于 this
的指向是不确定的,所以切勿在函数中包含多层的this
。
var o = {
f1: function () {
console.log(this) // {f1: ƒ}
var f2 = function () {
console.log(this) // Window
}()
}
}
o.f1()
f2
在编译的时候提升到全局,所以是 window
。如果要使用 o
作为 this
,可以在 f1
记录 this
然后使用。使用一个变量固定 this
的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的 this
指向顶层对象,就会报错。
const counter = { count: 0 }
counter.inc = function () {
'use strict'
this.count++
}
const f = counter.inc
f()
// TypeError: Cannot read properties of undefined (reading 'count')
注意数组处理方法中的this
数组的 map
和 foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用 this
。
const o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this, `${this.v} ${item}`)
})
}
}
o.f()
// window, undefined a1
// window, undefined a2
forEach
方法会调用数组中每一项的 toString
方法,所以 this
指向顶层对象 window
。解决办法同样可以记录 f
的 this
,然后使用。另一种方法是将 this
当作 foreach
方法的第二个参数,固定它的运行环境。
注意回调函数中的this
回调函数中的 this
往往会改变指向,最好避免使用。
var o = new Object()
o.f = function () {
console.log(this) // $('#button')
}
// jQuery 写法
$('#button').on('click', o.f)
点击按钮以后,此时 this
不再指向 o
对象,而是指向按钮的 DOM
对象,因为f方法是在按钮对象的环境中被调用的。为了解决这个问题,可以采用 call
、apply
、bind
方法对 this
进行绑定,也就是使得 this
固定指向某个对象,减少不确定性。