什么是this
JavaScript中的this是什么?
定义:this是包含它的函数作为方法被调用时所属的对象。
function fn1(){
this.name = "halo";
}
fn1();
- 我们将定义拆分一下
- 包含它的函数:包含this的函数是fn1。
- 作为方法被调用:fn1(); 此处fn1函数被调用。
- 所属的对象:函数式调用函数默认所属的对象是window。
通过上面三点分析,很容易知道fn1函数里的this指向的是window。
那么如果是更复杂的场景我们如何判断this的指向呢?
this到底指向谁
如果想用一句话总结this的指向,稍微了解一些this
指向的人都能脱口而出
谁调用它,this就指向谁。
也就是说this
的指向是在调用时确定的,而不是在定义时确定的。这么说没有错,但是并不全面。
其实,调用函数会创建新的术语函数自身的执行上下文。执行上下文的调用创建阶段会决定this
的指向。所以更加准确的总结应该是:
this的指向,是在调用函数时根据执行上下文所动态确定的。
在es6箭头函数之前,想要判断一个函数内部this指向谁,就根据以下四种方式来决定的。
- 函数式调用
- 上下文对象调用
- 构造函数调用
- bind、call、apply改变this指向
1、函数式调用
先来看一种相对简单的情况,函数在全局环境中被直接调用,严格模式下函数内this指向undefined,非严格模式下函数内this指向window。如下
function fn1() {
console.log(this)
}
function fn2() {
'use strict'
console.log(this)
}
fn1() // window
fn2() // undefined
再看下面例子:
const age = 18;
const p = {
age:15,
say:function(){
console.log(this)
console.log(this.age)
}
}
var s1 = p.say
s1()
这里say
方法内的this
仍然指向window
,因为p
中的say
函数赋值给s1
后,s1
的执行仍然是在window
的全局环境中。因此上面的代码最后输出window
和undefined
。
这里可能有人会有疑问,如果是在全局环境中,那this.age
不是应该输出18么?这是因为使用const
声明的变量不会挂载到window
全局对象上,因此this
指向window
时找不到window
上的age
。换成var声明即可输出18.
如果想让代码输出p
中的age
,并且让say函数中的this指向对象p
。只需要改变函数的调用方式,如下
const age = 18;
const p = {
age:15,
say:function(){
console.log(this)
console.log(this.age)
}
}
p.say()
输出
{age: 15, say: ƒ}
15
因为此刻say
函数内部的this
指向的是最后调用它的对象。再次验证了那句话,this的指向,是在调用函数时根据执行上下文所动态确定的。
2、上下文对象调用
const p = {
age: 18,
fn: function() {
return this
}
}
console.log(p.fn() === p)
输出
true
如果第一节的函数式调用理解了。那么这里应该也不会有疑问。
我们再重复一遍this的指向,是在调用函数时根据执行上下文所动态确定的。
记住这句话后遇到更复杂的场景也可以很容易的确定this指向。
如下:
const p = {
age: 20,
child: {
age: 18,
fn: function() {
return this.age
}
}
}
console.log(p.child.fn())
不论浅套关系如何变化,this都只想最后调用它的对象,因此输出18。
再升级下代码:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())
输出结果
o1
o1
undefined
- 第一个,应该没有问题,直接找到调用this的那个函数。
- 第二个,看似调用
o2.fn()
,其实内部调用的是o1.fn()
,因此还是输出o1
。 - 第三个,赋值后调用
fn()
,相当于在全局环境调用函数。this
指向window
。
如果现在的需求是想让
console.log(o2.fn())
输出o2,代码该如何修改?
如下:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn())
3、构造函数调用
function Foo() {
this.age = 18
}
const instance = new Foo()
console.log(instance.age)
输出18。知道输出结果并不难,但是new
操作符调用构造函数时都做了什么呢?
- 创建一个新对象;
- 将构造函数的
this
指向这个新对象; - 为新对象添加属性、方法;
- 返回新对象。
需要注意的是,如果在构造函数中出现显式的return
,那么就要分为两种场景分析。
function Foo(){
this.age = 18
const o = {}
return o
}
const instance = new Foo()
console.log(instance.age)
将会输出undefined
,因为如果在构造函数中出现显式的return
,并且返回一个对象时,那么创建的构造函数实例就是return
返回的对象,这里instance
就是返回的空对象o
。
function Foo(){
this.age = 18
return 1
}
const instance = new Foo()
console.log(instance.age)
将会输出18
,因为如果构造函数中出现显式return
,但是返回一个非对象的值时,那么this
还是指向实例。
总结:当构造函数显式返回一个值,并且返回的是一个对象,那么this
就指向这个返回的对象。如果返回的不是一个对象,this
仍然指向实例。
4、bind、call、apply改变this指向
关于基础用法,这里不再赘述。需要知道的是bind/call/apply
三者都是改变函数this
指向的,call/apply
是改变的同时直接进行函数调用,而bind
只是改变this
指向,并且返回一个新的函数,不会调用函数。call
和apply
的区别就是参数格式不同。详见如下代码:
const target = {}
fn.call(target, 'arg1', 'arg2')
上述代码等同于如下代码
const target = {}
fn.apply(target, ['arg1', 'arg2'])
可以看出知识调用的参数形式不同而已,改写成bind如下所示
const target = {}
fn.bind(target, 'arg1', 'arg2')()
不光要调用bind传入参数,还是在调用bind后再次执行函数。
明白call/apply/bind的使用后,再来看一段代码:
const foo = {
age: 18,
showAge: function() {
console.log(this.age)
}
}
const target = {
age: 22
}
console.log(foo.showAge.call(target))
结果输出22,只要掌握了call/apply/bind
的基本用法,对于输出结果并不难理解。我们往往会遇到多种方式同时出现的情况,我们在说完箭头函数的this
后会再详细说明this
优先级相关内容。
5、箭头函数this指向
熟悉es6的人应该会知道箭头函数中的this
指向,不再遵从上述的规制,而是根据外层的上下文来决定。
es5代码:
const foo = {
fn: function () {
setTimeout(function() {
console.log(this)
})
}
}
foo.fn() // Window{……}
this
出现在setTimeout()
中的匿名函数里时,this
指向window
对象。这种特性势必会给我们的开发带来一些坑,es6的箭头函数就很好的解决了这个问题。
es6代码:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this)
})
}
}
foo.fn() // {fn: ƒ}
箭头函数中的this
指向,不再适用上面的标准,而是找到外层上下文,这段代码中this
在箭头函数中,则找到外层的上下文的调用对象——foo
。因此这里的this
指向的就是foo
。
注意:当箭头函数改变了this
指向后,那么该this
指向就不再受任何影响,也就是说不会再次发生改变,具体在this
优先级章节中会举例说明。
总结:
- 通过
call、apply、bind、new
等改成this
指向的操作称为显式绑定; - 根据上下文关系确定的
this
指向成为隐式绑定。
如果一段代码中即出现显式绑定又有隐式绑定,该如何确定this
指向呢?
往下看
6、this优先级
function foo (age) {
console.log(this.age)
}
const o1 = {
age: 1,
foo: foo
}
const o2 = {
age: 2,
foo: foo
}
o1.foo.call(o2)
o2.foo.call(o1)
如果隐式绑定优先级高于显式绑定,那么应该输出1,2
。但是运行代码发现结果输出2,1
。这也就说明了显式绑定中的call、apply
优先级更高。
再看:
function foo (age) {
this.age = age
}
const o1 = {}
var fn = foo.bind(o1)
fn(18)
console.log(o1.age) // 18
var f1 = new fn(22)
console.log(f1.age); // 22
分析下上面代码,fn
是foo
函数调用bind
方法返回的函数,也就相当于是返回foo
函数,并且将this
指向o1
对象。执行了fn(18)
后o1
对象的age
值就是18了,所以第一个输出结果是18。
然后通过new
调用fn
函数,这时fn
函数作为构造函数被调用,this
就会指向返回的实例,从而与o1
对象解绑。
因此得出结论:new
的优先级高于bind
。
还记得上一节提到的箭头函数特行么?箭头函数影响的this指向无法被修改。看下面代码:
function foo() {
return () => {
console.log(this.age)
};
}
const o1 = {
age: 2
}
const o2 = {
age: 3
}
const fn = foo.call(o1)
console.log(fn.call(o2))
输出为2,foo
的this
指向了o1
,fn
接收的箭头函数的this
自然也会指向o1
。而箭头函数的this是不会再次改变的,所以尽管用显式绑定call
去改变this
指向,也是不起作用的。
结束啦!
this涉及知识点繁多,碰到优先级问题也是让人头疼。
没有什么捷径,唯有“死记硬背”+“慢慢理解”