this
是 JavaScript
语言的一个关键字,它代表函数运行时自动生成的一个内部对象,只能在函数内部使用,随着函数使用场合的不同,this
的值会发生变化,但是有一个总的原则,那就是 this
指向的是『调用函数的那个对象』
this 的调用方式
在 JavaScript
中函数的调用有以下几种方式
- 为对象方法调用
- 作为函数调用
- 作为构造函数调用
- 使用
apply
或call
调用
下面我们就按照调用方式的不同,分别来讨论各个情况当中的 this
含义
作为对象方法调用
在 JavaScript
中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,this
被自然绑定到该对象
1 | var point = { |
纯粹的函数调用
函数也可以直接被调用,此时 this
绑定到全局对象,在浏览器中,window
就是该全局对象,比如下面的例子,函数被调用时,this
被绑定到全局对象,接下来执行赋值语句,相当于隐式的声明了一个全局变量,这显然不是调用者希望的
1 | function makeNoSense(x) { |
对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式会产生另外一个问题,我们仍然以前面提到的 point
对象为例,这次我们希望在 moveTo
方法内定义两个函数,分别将 x
,y
坐标进行平移,结果可能出乎意料,不仅 point
对象没有移动,反而多出两个全局变量 x
,y
1 | var point = { |
内部函数中的 this
成为全局的了,为了规避这一设计缺陷,一般使用变量替代的方法,该变量常被命名为 that/_this/self
1 | var point = { |
一个简单的记忆方法,当函数当中嵌套函数就可能会形成闭包环境,这时的
this
指向就可能是window
了
作为构造函数调用
所谓构造函数,就是通过这个函数生成一个新对象(Object
),实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new
操作符调用的普通函数而已,包括内置对象函数在内的所有函数都可以用 new
来调用,这种函数调用被称为构造函数调用,实际上并不存在所谓的构造函数,只有对于函数的构造调用,使用 new
来调用函数,会自动执行以下操作
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行
[[原型]]
连接 - 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其他对象,那么
new
表达式中的函数会自动返回这个新对象
这时 this
就指这个新对象, 如果不使用 new
调用,则和普通函数一样
1 | function C() { |
使用 apply 或 call 调用
apply()
是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象,因此 this
指的就是这第一个参数
1 | function Point(x, y) { |
在上面的例子中,我们使用构造函数生成了一个对象 p1
,该对象同时具有 moveTo
方法,然后使用对象字面量创建了另一个对象 p2
,需要注意此时的 p2
是没有 moveTo
这个方法的,但是我们可以使用 apply
来将 p1
的方法应用到 p2
上,这时候 this
也被绑定到对象 p2
上,另一个方法 call
也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的
注意,当使用过程中参数为空时,默认调用全局对象,也就是下面这种情况
1 | function fun() { |
更为详细的内容可以参考规范当中的 Function.prototype.call()
四种方式的优先级
如下所示,优先级从上往下
- 由
new
调用绑定到新创建的对象 - 如果是由
call
或者apply
(或者bind
)调用则绑定到指定的对象 - 如果是由上下文对象调用则绑定到那个上下文对象
- 默认:在严格模式下绑定到
undefined
,否则绑定到全局对象
不太常见的调用方式
上文介绍了 this
比较常见的几种调用方式,下面来看看一些不太常见的场景
原型链中的 this
相同的概念在定义在原型链中的方法也是一致的,如果该方法存在于一个对象的原型链上,那么 this
指向的是调用这个方法的对象,表现得好像是这个方法就存在于这个对象上一样
1 | var o = { |
对象 p
没有属于它自己的 f
属性,它的 f
属性继承自它的原型,但是这对于最终在 o
中找到 f
属性的查找过程来说没有关系,查找过程首先从 p.f
的引用开始,所以函数中的 this
指向 p
,也就是说,因为 f
是作为 p
的方法调用的,所以它的 this
指向了 p
getter 与 setter 中的 this
作为 getter
或 setter
函数都会绑定 this
到从设置属性或得到属性的那个对象
1 | function modulus() { |
DOM 事件处理函数中的 this
当函数被用作事件处理函数时,它的 this
指向触发事件的元素,需要注意 IE
的 attachEvent()
中的 this
是指向 window
的(不过 IE11+
已经支持 addEventListener()
)
1 | // 被调用时,将关联的元素变成蓝色 |
内联事件处理函数中的 this
当代码被内联处理函数调用时,它的 this
指向监听器所在的 DOM
元素
1 | <button onclick="alert(this.tagName.toLowerCase())"> |
上面的 alert
会显示 button
,但是注意只有外层代码中的 this
是这样设置的,如果使用闭包(如下所示),则里面的 this
是指向 window/global
的
1 | <button onclick="alert((function(){return this})())"> |
箭头函数中的 this
下面我们在来看一种比较特殊的情况,即箭头函数当中的 this
,不过在此之前,我们先来了解一下什么是箭头函数,箭头函数是 ES6
当中新增的一种比函数表达式更简洁的语法,比如下面这个例子
1 | const 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 | function Person() { |
但是这里需要注意,在严格模式下,与 this
相关的规则都将被忽略
1 | var foo = () => { |
最后我们来看一个小例子,下面的输出结果是什么
1 | (() => { console.log(this) }()) |
运行以后可以发现,是会报错的,至于原因可以参考 这里,其实我们可以来把它还原一下就知道为什么了
1 | (() => { 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 | var foo = 1 |
又或者
1 | var foo = { |
而且规范中还提供了获取 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 | var foo = 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 | PrimaryExpression // 原始表达式,最简单的表达式,JavaScript 的原始表达式包含常量或直接量、关键字和变量 |
好像是看到了 MemberExpression
的身影,我们通过几个例子来看看
1 | function foo() { |
通过例子可以发现,其实可以简单的理解为,MemberExpression
其实就是 ()
左边的部分
计算 Reference
现在到了最关键的一步,即判断 ref
是不是一个 Reference
类型,如果知道了 Reference
的类型,那么我们就可以得出对应的 this
的值,还是老规矩,我们通过实际的示例来进行了解
1 | var value = 1 |
根据之前的内容,我们可以知道
1 | var Reference = { |
所以我们就依次来看上面的五个示例
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 | var value = 1 |
此外,还有一个比较常见的情况
1 | function 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 | var fooReference = { |
又因为 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 |