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 valueIsPropertyReference(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 |