JavaScript 中的 this

JavaScript 中的 this

thisJavaScript 语言的一个关键字,它代表函数运行时自动生成的一个内部对象,只能在函数内部使用,随着函数使用场合的不同,this 的值会发生变化,但是有一个总的原则,那就是 this 指向的是『调用函数的那个对象』

this 的调用方式

JavaScript 中函数的调用有以下几种方式

  • 为对象方法调用
  • 作为函数调用
  • 作为构造函数调用
  • 使用 applycall 调用

下面我们就按照调用方式的不同,分别来讨论各个情况当中的 this 含义

作为对象方法调用

JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,this 被自然绑定到该对象

1
2
3
4
5
6
7
8
9
10
11
var point = {
x: 0,
y: 0,
moveTo: function (x, y) {
this.x = this.x + x
this.y = this.y + y
}
}

// this 绑定到当前对象,即 point 对象
point.moveTo(1, 1)

纯粹的函数调用

函数也可以直接被调用,此时 this 绑定到全局对象,在浏览器中,window 就是该全局对象,比如下面的例子,函数被调用时,this 被绑定到全局对象,接下来执行赋值语句,相当于隐式的声明了一个全局变量,这显然不是调用者希望的

1
2
3
4
5
6
function makeNoSense(x) {
this.x = x
}

// 此时的 x 已经成为一个值为 5 的全局变量
makeNoSense(5)

对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式会产生另外一个问题,我们仍然以前面提到的 point 对象为例,这次我们希望在 moveTo 方法内定义两个函数,分别将 xy 坐标进行平移,结果可能出乎意料,不仅 point 对象没有移动,反而多出两个全局变量 xy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var point = {
x: 0,
y: 0,
moveTo: function (x, y) {
var moveX = function (x) {
this.x = x
}
var moveY = function (y) {
this.y = y
}
moveX(x)
moveY(y)
}
}

point.moveTo(1, 1)

point.x // ==> 0
point.y // ==> 0

x // ==> 1
y // ==> 1

内部函数中的 this 成为全局的了,为了规避这一设计缺陷,一般使用变量替代的方法,该变量常被命名为 that/_this/self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var point = {
x: 0,
y: 0,
moveTo: function (x, y) {
var that = this
var moveX = function (x) {
that.x = x
}
var moveY = function (y) {
that.y = y
}
moveX(x)
moveY(y)
}
}

point.moveTo(1, 1)

point.x // ==> 1
point.y // ==> 1

一个简单的记忆方法,当函数当中嵌套函数就可能会形成闭包环境,这时的 this 指向就可能是 window

作为构造函数调用

所谓构造函数,就是通过这个函数生成一个新对象(Object),实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已,包括内置对象函数在内的所有函数都可以用 new 来调用,这种函数调用被称为构造函数调用,实际上并不存在所谓的构造函数,只有对于函数的构造调用,使用 new 来调用函数,会自动执行以下操作

  • 创建(或者说构造)一个全新的对象
  • 这个新对象会被执行 [[原型]] 连接
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式中的函数会自动返回这个新对象

这时 this 就指这个新对象, 如果不使用 new 调用,则和普通函数一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function C() {
this.a = 37
}

var o = new C()
console.log(o.a) // 37


function C2() {
this.a = 37
return { a: 38 } // 手动的设置了返回对象,与 this 绑定的默认对象被取消
}

o = new C2()
console.log(o.a) // 38

使用 apply 或 call 调用

apply() 是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象,因此 this 指的就是这第一个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Point(x, y) {
this.x = x
this.y = y
this.moveTo = function (x, y) {
this.x = x
this.y = y
}
}

var p1 = new Point(0, 0)
var p2 = { x: 0, y: 0 }

p1.moveTo(1, 1)
p1.moveTo.apply(p2, [10, 10])

在上面的例子中,我们使用构造函数生成了一个对象 p1,该对象同时具有 moveTo 方法,然后使用对象字面量创建了另一个对象 p2,需要注意此时的 p2 是没有 moveTo 这个方法的,但是我们可以使用 apply 来将 p1 的方法应用到 p2 上,这时候 this 也被绑定到对象 p2 上,另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的

注意,当使用过程中参数为空时,默认调用全局对象,也就是下面这种情况

1
2
3
4
5
6
function fun() {
alert(this)
}

fun.call(null) // window
fun.call(undefined) // window

更为详细的内容可以参考规范当中的 Function.prototype.call()

四种方式的优先级

如下所示,优先级从上往下

  1. new 调用绑定到新创建的对象
  2. 如果是由 call 或者 apply(或者 bind)调用则绑定到指定的对象
  3. 如果是由上下文对象调用则绑定到那个上下文对象
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象

不太常见的调用方式

上文介绍了 this 比较常见的几种调用方式,下面来看看一些不太常见的场景

原型链中的 this

相同的概念在定义在原型链中的方法也是一致的,如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,表现得好像是这个方法就存在于这个对象上一样

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
f: function () {
return this.a + this.b
}
}

var p = Object.create(o)

p.a = 1
p.b = 4

console.log(p.f()) // 5

对象 p 没有属于它自己的 f 属性,它的 f 属性继承自它的原型,但是这对于最终在 o 中找到 f 属性的查找过程来说没有关系,查找过程首先从 p.f 的引用开始,所以函数中的 this 指向 p,也就是说,因为 f 是作为 p 的方法调用的,所以它的 this 指向了 p

getter 与 setter 中的 this

作为 gettersetter 函数都会绑定 this 到从设置属性或得到属性的那个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function modulus() {
return Math.sqrt(this.re * this.re + this.im * this.im)
}

var o = {
re: 1,
im: -1,
get phase() {
return Math.atan2(this.im, this.re)
}
}

Object.defineProperty(o, 'modulus', {
enumerable: true,
configurable: true,
get: modulus
})

// -0.78... 1.4142...
console.log(o.phase, o.modulus)

DOM 事件处理函数中的 this

当函数被用作事件处理函数时,它的 this 指向触发事件的元素,需要注意 IEattachEvent() 中的 this 是指向 window 的(不过 IE11+ 已经支持 addEventListener()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 被调用时,将关联的元素变成蓝色
function bluify(e) {
// 总是 true
console.log(this === e.currentTarget)

// 当 currentTarget 和 target 是同一个对象是为 true
console.log(this === e.target)
this.style.backgroundColor = '#A5D9F3'
}

// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*')

// 将 bluify 作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', bluify, false)
}

内联事件处理函数中的 this

当代码被内联处理函数调用时,它的 this 指向监听器所在的 DOM 元素

1
2
3
<button onclick="alert(this.tagName.toLowerCase())">
Show inner this
</button>

上面的 alert 会显示 button,但是注意只有外层代码中的 this 是这样设置的,如果使用闭包(如下所示),则里面的 this 是指向 window/global

1
2
3
<button onclick="alert((function(){return this})())">
Show inner this
</button>

箭头函数中的 this

下面我们在来看一种比较特殊的情况,即箭头函数当中的 this,不过在此之前,我们先来了解一下什么是箭头函数,箭头函数是 ES6 当中新增的一种比函数表达式更简洁的语法,比如下面这个例子

1
2
3
4
5
6
7
const foo = val => console.log(val)

// 等同于 ==>

function foo(val) {
console.log(val)
}

我们在这里主要关注箭头函数当中的 this 指向和它与普通函数之前的区别,所以关于箭头函数更为详细的内容可以参考 箭头函数

箭头函数与普通函数之间的差异

主要涉及以下几点

  • 因为箭头函数没有 this,所以也不能用 call()apply()bind() 这些方法改变 this 的指向
  • 没有 arguments,但是箭头函数可以访问外围函数的 arguments 对象
  • 箭头函数不能用作构造器,和 new 一起用会抛出错误
  • 因为不能使用 new 调用,所以也没有 new.target 值,也就没有 prototype 属性
  • 没有原型,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 argumentsnew.target 一样,这些值由外围最近一层非箭头函数决定
  • yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内),因此箭头函数不能用作函数生成器

下面我们来着重看一下箭头函数当中的 this

没有 this

箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值,这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this,简单来说就是,箭头函数不会创建自己的 this,它会从自己的作用域链的上一层继承 this,比如下面这个例子,其中的 this 会正确地指向 p 实例

1
2
3
4
5
6
7
8
function Person() {
this.age = 0
setInterval(() => {
this.age++
}, 1000)
}

var p = new Person()

但是这里需要注意,在严格模式下,与 this 相关的规则都将被忽略

1
2
3
4
5
6
7
8
9
10
11
12
var foo = () => {
'use strict'
return this
}

var bar = function () {
'use strict'
return this
}

foo() === window // true
bar() === undefined // true

最后我们来看一个小例子,下面的输出结果是什么

1
(() => { console.log(this) }())

运行以后可以发现,是会报错的,至于原因可以参考 这里,其实我们可以来把它还原一下就知道为什么了

1
2
3
4
5
6
7
(() => { console.log(this) }())

// ==> 等同于
(function f() { console.log(this) }())

// ==> 也就是
function f() { console.log(this) } ()

可以发现,其实写法是有问题的,至于解决办法,可以采用下面这种方式,用括号将函数包裹起来

1
((() => { console.log(this) })())

深入 this

在前文部分,我们花了大量章节来对各个场景下的 this 做了介绍,总结了 this 在不同场景下的指向结果,但是都没有从根本上解释现象出现的原因,所以今天我们就借助 ECMAScript 规范 来深入的来了解一下,到底 this 是个什么东西,它是如何来进行定义的,在规范当中规定,ECMAScript 有三种可执行代码

  • 全局代码(Global code
  • eval 代码(Eval code
  • 函数代码(Function code

其中,对于全局代码直接指向 windoweval 代码由于已经不推荐使用我们就暂不做讨论,所以我们主要关注点就是函数代码中的 this 如何指定

函数调用

规范指出,当执行流进入函数代码时,由函数调用者提供 thisArgargumentsList,在 11.2.3 函数调用 当中我们可以发现,在函数调用发生时,首先会对『函数名部分进行计算』并赋值给 ref,并且通过一系列的判断就可以来决定 this 会指向何方,如下图所示

但是在展开之前,我们需要先来了解一下什么是 Reference

Type(ref) is Reference

8.7 引用规范类型 当中可知,Reference 的构成,由三个组成部分

  • base value,指向引用的原值
  • referenced name,引用的名称
  • strict reference flag,标示是否严格模式

简单来说就是,规范中定义了一种类型叫做 Reference,作用是用来引用其他变量,它有一个规定的数据结构,base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefinedan Objecta Booleana Stringa Numberor an Environment Record 其中的一种

词法环境为环境记录项 Environment Record 的组成,它是规范用来管理当前作用域下面变量的类型,了解即可

referenced name 就是属性的名称,比如

1
2
3
4
5
6
7
8
var foo = 1

// 对应的 Reference 为
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
}

又或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {
bar: function () {
return this
}
}

foo.bar() // foo

// bar 对应的 Reference 为
var BarReference = {
base: foo,
propertyName: 'bar',
strict: false
}

而且规范中还提供了获取 Reference 组成部分的方法,方法有很多,但是这里我们仅仅关心下面这几个方法

  • GetBase(V),返回 referencebase value
  • IsPropertyReference(V),如果 base value 是个对象或 HasPrimitiveBasetrue,那么返回 true,否则返回 false
    • 在这里,我们可以简单的理解为,如果其 base value 是一个对象,那么就返回 true
  • HasPrimitiveBase(V),如果 base value 是布尔,字符串,数值,那么返回 true

GetValue

在规范 8.7.1 GetValue(v) 当中提供了一个用于从 Reference 类型获取对应值的方法 GetValue,该方法会返回对象属性真正的值,简单来说就是

1
2
3
4
5
6
7
8
9
var foo = 1

var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
}

GetValue(fooReference) // 1

但是要注意:调用 GetValue,返回的将是具体的值,而『不再是』一个 Reference(这个很重要,下面示例当中会多次用到)

下面我们就来正式的了解一下,如何确定 this 的值

确定 this 的值

11.2.3 函数调用 当中,我们可以了解到可以如何来确定 this 的值,其实也就是上面图中所表达的意思,我们简单的总结一下就是

  • 计算 MemberExpression 的结果赋值给 ref
  • 判断 ref 是不是一个 Reference 类型
    • 如果 refReference,并且 IsPropertyReference(ref)true,那么 this 的值为 GetBase(ref)
    • 如果 refReference,并且 base value 值是 Environment Record,那么 this 的值为 ImplicitThisValue(ref)
    • 如果 ref 不是 Reference,那么 this 的值为 undefined

下面我们对照上面提到的步骤,一步一步的详细来看

MemberExpression

首先第一步就是计算 MemberExpression 的结果赋值给 ref,那么什么是 MemberExpression 呢?我们来看规范 11.2 左值表达式

1
2
3
4
5
PrimaryExpression                 // 原始表达式,最简单的表达式,JavaScript 的原始表达式包含常量或直接量、关键字和变量
FunctionExpression // 函数定义表达式,var f = function() { ... }
MemberExpression [ Expression ] // 属性访问表达式,用 [] 的方式访问值
MemberExpression . IdentifierName // 属性访问表达式,用 . 的方式访问值
new MemberExpression Arguments // 对象创建表达式,使用 new

好像是看到了 MemberExpression 的身影,我们通过几个例子来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo() {
console.log(this)
}
// MemberExpression 是 foo
foo()


function foo() {
return function () {
console.log(this)
}
}
// MemberExpression 是 foo()
foo()()


var foo = {
bar: function () {
return this
}
}
// MemberExpression 是 foo.bar
foo.bar()

通过例子可以发现,其实可以简单的理解为,MemberExpression 其实就是 () 左边的部分

计算 Reference

现在到了最关键的一步,即判断 ref 是不是一个 Reference 类型,如果知道了 Reference 的类型,那么我们就可以得出对应的 this 的值,还是老规矩,我们通过实际的示例来进行了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var value = 1

var foo = {
value: 2,
bar: function () {
return this.value
}
}

console.log(foo.bar()) // 示例 1
console.log((foo.bar)()) // 示例 2
console.log((foo.bar = foo.bar)()) // 示例 3
console.log((false || foo.bar)()) // 示例 4
console.log((foo.bar, foo.bar)()) // 示例 5

根据之前的内容,我们可以知道

1
2
3
4
5
var Reference = {
base: foo,
name: 'bar',
strict: false
}

所以我们就依次来看上面的五个示例

foo.bar()

第一个我们就慢慢来看,根据上面的流程可知,我们首先需要做的工作就是计算 MemberExpression 的结果,通过计算可以得出,第一个示例的 MemberExpression 的结果是 foo.bar,即是通过属性表达式来进行访问的,而我们在之前的左值表达式当中已经介绍过了,该表达式返回了一个 Reference 类型,所以我们第一步的工作已经完成了,即确定了它是 Reference 类型

有了 ref 以后,所以接下来我们就可以先来进行第一次的判断,即判断 IsPropertyReference(ref) 的值,通过之前的内容我们可以知道,如果 IsPropertyReferencebase value 是一个对象,那么就返回 true,观察可知该示例的 base valuefoo,是一个对象,所以 IsPropertyReference(ref) 结果为 true,到了这一步,就不用继续往下走了,我们已经可以断定 this 的值了,也就是取 GetBase(ref),而 GetBase(ref) 就是返回 referencebase value,所以 this 的值就是 foo

绕了一个大弯,我们终于知道了第一个示例的结果,不过既然已经知道了流程,那么剩下的解决起来就很快了

(foo.bar)()

foo.bar() 包住,通过 11.1.6 分组表达式 可知

返回执行 Expression 的结果,它可能是 Reference 类型

注:这一算法并不会作用 GetValue 于执行 Expression 的结果

() 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的

(foo.bar = foo.bar)()

我们可以发现,在这个示例当中有赋值操作符,所以我们查看 11.13.1 简单赋值 可知

rvalGetValue(rref)

简单赋值语句返回的是针对 = 号右边进行 GetValue 之后的结果,又因为调用 GetValue 后,返回的值『不再是』一个 Reference,所以根据 MemberExpression 我们可以得出,如果 ref 不是 Reference,那么 this 的值为 undefined,这里需要注意,如果在非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象

(false || foo.bar)()

同示例 3 类似,不过这里调用的不再是赋值操作,而是逻辑与算法,同样的,我们查看 11.11 二元逻辑运算符 可知

lvalGetValue(lref)

因为使用了 GetValue,所以返回的不是 Reference 类型,thisundefined

(foo.bar, foo.bar)()

逗号操作符,我们查看 11.14 逗号运算符 可知

Call GetValue(lref)

因为使用了 GetValue,所以返回的不是 Reference 类型,thisundefined

结果汇总

综上所述,示例的运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var value = 1

var foo = {
value: 2,
bar: function () {
return this.value
}
}

console.log(foo.bar()) // 示例 1,结果为 2
console.log((foo.bar)()) // 示例 2,结果为 2
console.log((foo.bar = foo.bar)()) // 示例 3,结果为 1(这里需要注意,严格模式下因为 this 返回 undefined,所以示例 3 会报错)
console.log((false || foo.bar)()) // 示例 4,结果为 1
console.log((foo.bar, foo.bar)()) // 示例 5,结果为 1

此外,还有一个比较常见的情况

1
2
3
4
5
function foo() {
console.log(this)
}

foo()

我们可以知道,MemberExpression 的结果 foo,通过 11.1.2 标识符引用 可知

执行遵循 10.3.1 所规定的标识符查找,标识符执行的结果总是一个 Reference 类型的值

继续查看 10.3.1 所代表的标识符解析可知

envIdentifierstrict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果

继续查看 GetIdentifierReference,见 10.2.2.1 GetIdentifierReference(lex, name, strict)

返回一个类型为 Reference 的对象,其 base valueenvRecReference 的名称为 name,严格模式标识的值为 strict

可以看到返回了一个 Reference,而 其 base valueenvRec 也就是 10.3.1 中传入的 lexexecution context’s LexicalEnvironment),所以其抽象数据结构为下

1
2
3
4
5
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
}

又因为 ref 是一个 Reference,但是其 base value 却是 EnvironmentRecord,并不是一个对象类型,所以不会走 IsPropertyReference(ref) 的判断,而是使用 ImplicitThisValue(ref) 来界定 this 的值,同样的查看 10.2.1.2.6 ImplicitThisValue() 可知

始终返回 undefined 作为其 ImplicitThisValue

所以最后 this 的值就是 undefined

总结

经过上面的一些示例,我们可以大致总结出一个规律,其实最关键的就是判断返回值是不是 reference,如果不是,直接可以推出等于 window,如果是则只需要看是不是属性 reference,然后在进行判断,但是每次查询规范也有点麻烦,所以就有外国友人帮我们整理了一张速查表

Example Reference? Notes
'foo' No
123 No
/x/ No
({}) No
(function(){}) No
foo Yes Could be unresolved reference if foo is not defined
foo.bar Yes Property reference
(123).toString Yes Property reference
(function(){}).toString Yes Property reference
(1,foo.bar) No Already evaluated, BUT see grouping operator exception
(f = foo.bar) No Already evaluated, BUT see grouping operator exception
(foo) Yes Grouping operator does not evaluate reference
(foo.bar) Yes Ditto with property reference

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×