JavaScript 中的 this

JavaScript 中的 this

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

this 的调用方式

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

  • 为对象方法调用
  • 作为函数调用
  • 作为构造函数调用
  • 使用 apply 或 call 调用

下面我们就按照调用方式的不同,分别来讨论各个情况当中的 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 方法内定义两个函数,分别将 x,y 坐标进行平移,结果可能出乎意料,不仅 point 对象没有移动,反而多出两个全局变量 x,y

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

作为 getter 或 setter 函数都会绑定 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 指向触发事件的元素,需要注意 IE 的 attachEvent() 中的 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 的,不过跟 arguments、new.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)

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

函数调用

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

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

Type(ref) is Reference

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

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

简单来说就是,规范中定义了一种类型叫做 Reference,作用是用来引用其他变量,它有一个规定的数据结构,base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined,an Object,a Boolean,a String,a Number,or 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),返回 reference 的 base value
  • IsPropertyReference(V),如果 base value 是个对象或 HasPrimitiveBase 是 true,那么返回 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 类型
    • 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true,那么 this 的值为 GetBase(ref)
    • 如果 ref 是 Reference,并且 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) 的值,通过之前的内容我们可以知道,如果 IsPropertyReference 的 base value 是一个对象,那么就返回 true,观察可知该示例的 base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true,到了这一步,就不用继续往下走了,我们已经可以断定 this 的值了,也就是取 GetBase(ref),而 GetBase(ref) 就是返回 reference 的 base value,所以 this 的值就是 foo

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

(foo.bar)()

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

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

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

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

(foo.bar = foo.bar)()

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

令 rval 为 GetValue(rref)

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

(false || foo.bar)()

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

令 lval 为 GetValue(lref)

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

(foo.bar, foo.bar)()

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

Call GetValue(lref)

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

结果汇总

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

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 所代表的标识符解析可知

以 env、Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果

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

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

可以看到返回了一个 Reference,而 其 base value 是 envRec 也就是 10.3.1 中传入的 lex(execution 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

×