前言
终于写完了,中途被绕晕了好几次不想写了,然后又作死的往不太清楚的地方编题,但被绕晕的原因终究是没弄懂箭头函数的指向,所以硬着头皮写下来了,算是我创作中耗费时间最久的一篇。
如果你此时也对this指向不是很清楚或者想练练this指向的题,请耐心读下去,希望对你有帮助。
this指向问题
关于 this
的指向问题,在笔试或者面试中常考的方面是有关箭头函数和普通函数之间的 this
指向,因此必须得记住下面三点
(1) 箭头函数是没有自己的 this ,而是继承外层最近的普通函数的上下文 this(如果外层没有普通函数,最终就会指向全局的上下文,在浏览器中即 window )。
(2) 箭头函数在被定义(或被创建)的时候就确认了 this 的指向(即包含箭头函数的上下文也就是箭头函数外层的 this),不管被谁调用的。
(3) 这个 this 的指向是静态的,改变不了。
普通函数则是最终谁调用的,this 就指向谁。如果最终没有谁调用,那就是由 window 调用的。
一阶------普通函数和箭头函数this的指向
js
// 普通函数
function func1() {
console.log(this, '1');
};
// 箭头函数
const func2 = () => {
console.log(this, '2');
}
func1() // window
func2() // window
普通函数是由谁调用,this
就指向谁,而箭头函数根据一开始的说法,定义的时候就确认了 this(即上下文)
普通函数 func1
由于是 window
调用的(即window.func1()
),所以指向 window
。
func2
是箭头函数,根据箭头函数是没有自己的 this ,而是继承外层最近的普通函数的上下文 this(如果外层没有普通函数,最终就会指向全局的上下文,在浏览器中即 window ) ,因此它的 this
指向的是 window
。
二阶------在对象中判断普通函数和箭头函数的this指向(构造函数呢?)
函数在对象里创建
js
let obj = {
sex: "1",
func1: function() {
console.log(this, this.sex)
},
func2: () => {
console.log(this, this.sex)
},
}
obj.func1() // obj, 1
obj.func2() // window, undefined
普通函数经典的谁调用,this
就指向谁,obj
调用的普通函数 func1
,所以指向 obj
。
由于箭头函数在被定义(或被创建)的时候就确认了 this 的指向 ,因此我们不管谁调用的,直接分析 obj
定义里的箭头函数,而箭头函数在 obj
中创建时,相当于在 obj
对象中创建了箭头函数 func2
,而不是在普通函数 func2
中创建的(别钻进了牛角尖 ),func2
只是一个声明的 key
值,类似下面的伪代码。
js
let obj = {
() => {console.log(this)} // 但由于箭头函数是匿名函数,所以得声明一个func2
}
而又由于箭头函数没有自己的 this ,而是继承最近的普通函数的上下文 this ,所以它就往外找啊,先找到了 obj
,但由于 obj
不是普通函数,所以 this
继续往外找,因此最后 func2
里的 this
指向 window
。
思考:在 window 里定义的箭头函数和在 obj 里定义的箭头函数,this 指向为什么是一样的?
js
const func2 = () => {
console.log(this);
}
const obj = {
func2: () => {
console.log(this);
}
}
func2() // window
obj.func2() // window
第一个 func2
,根据箭头函数是没有自己的 this ,而是继承外层最近的普通函数的上下文 this ,判断出 this
指向的是 window
这个上下文。
第二个 func2
同理,只是包了一层对象 obj
而已,但没有影响,obj
连函数都不算,所以 this
最终指向 window
。
同理,就算 obj
外面在包一层对象,箭头函数一样是指向 window
的。
js
const parent = {
sex: "1",
obj: {
sex: "2",
func1: function() {
console.log(this, this.sex)
},
func2: () => {
console.log(this, this.sex);
}
}
}
parent.obj.func1() // obj, 2
parent.obj.func2() // window, undefined
普通函数仍然是谁调用就指向谁 ,不必在意普通函数所在的上下文,因此只需弄清最后谁调用方法就能知道 this
指向谁。parent.obj.func1()
这个过程,是谁调用了 func1()
,一看就知道是 parent.obj
,是 parent
里的 obj
,因此 this
是指向 obj
的。其实只是通过 parent
拿到了 obj
这个对象,然后再通过 obj
这个对象调用了 func1
,所以这个过程中,parent
其实只是单纯的取值,真正调用方法的其实是 obj
。
在 func2
中,箭头函数继承外层最近的普通函数的上下文 this
。在这个例子中,由于箭头函数 func2
是在对象字面量内部定义的,而该对象字面量外层没有非箭头函数,因此箭头函数中的 this
将指向全局对象即 window
。
函数在对象外创建
js
const test1 = function() {
console.log(this,this.sex)
}
const test2 = () => {
console.log(this,this.sex)
}
const obj = {
sex: 1,
func1: test1,
func2: test2
}
obj.func1(); // obj, 1
obj.func2(); // window, undefined
同上述一种情况类似,普通函数func1
最终由谁调用,this
就指向谁,而箭头函数 func2
则是在创建时就确定了上下文 ,即 window
(继承最近的普通函数的上下文 this ),那么 this
也就指向了window
三阶------改变了this的指向
把对象里的函数赋值给一个新的变量
js
const obj = {
func1: function() {
console.log(this, '1');
},
func2: () => {
console.log(this, '2');
}
};
// 第一组
obj.func1() // obj
obj.func2() // window
// 第二组
const fn1 = obj.func1;
const fn2 = obj.func2;
fn1(); // window
fn2(); // window
第一组不分析,就是二阶里的,放这里只是为了误导和对比,直接分析第二组二阶------改变指向
第一个,先是通过 obj.func1
拿到了普通函数(但是,它还没调用呢,this
还不急着确认),然后把 obj.func1
赋值给了变量fn1
(也就是把 obj
里的 func1
的函数赋值给了变量 fn1
),使 fn1
成为了一个函数,这时再调用 fn1
,就相当于 window.fn1()
,也就是 window
调用了这个函数,那么 this
就指向了最终调用的 window
了
而 fn2()
,它是 obj.func2
赋值给的 fn2
,由于 obj
里的 func2
是箭头函数,它在定义的时候就确认了 this 指向,并且它是继承最近的普通函数的上下文 this ,而且这个 this 指向是静态的 ,不管谁调用 、怎么改变方向都没用,因此 this
指向一开始的定义时就确定好的 this
指向 window
。
把对象外的函数赋值给一个新的变量
js
const test1 = function() {
console.log(this, '1')
}
const test2 = () => {
console.log(this, '2')
}
const obj = {
func1: test1,
func2: test2
};
// 第一组
obj.func1() // obj
obj.func2() // window
// 第二组
const fn1 = obj.func1;
const fn2 = obj.func2;
fn1(); // window
fn2(); // window
同上,第一组不分析,直接分析第二组。
先是在 window
中创建了普通函数 test1
,然后把 test1
赋值给了对象 obj
中的 func1
,接着再通过 obj
对象拿到普通函数 func1
,并赋值给了 fn1
,使 fn1
光荣的成为了一名普通函数,最后,不管普通函数 test1
, func1
还是 fn1
,它们本质上就是个普通函数,因此 fn1()
最终是由 window
调用的(window.fn1()
),所以 this
指向 window
而 test2
是在 window
下创建的,根据箭头函数在被定义(或被创建)的时候就确认了 this 的指向(即包含箭头函数的上下文也就是箭头函数外层的 this ),并且 this 是静态的 ,因此不管箭头函数怎么被赋值改变,this
仍然指向箭头函数 test2
被创建时的上下文 window
通过 apply , call,bind 改变 this 的指向
以下以 apply
为例,call
、 bind
同理。
js
const obj = {
sex: 1
}
// 普通函数
function func1() {
console.log(this, '1');
};
// 箭头函数
const func2 = () => {
console.log(this, '2');
}
// 第一组
func1() // window
func2() // window
// 第二组
func1.apply(obj) // obj
func2.apply(obj) // window
func1.apply(obj).apply()
func2.apply(obj).apply()
同上,第一组不分析,它是一阶里的,直接分析第二组。
func1
虽然是普通函数,按照原本分析是由 window
调用的,但由于使用了 apply
强制改变了 func1
的 this
指向,即 apply
函数的参数 obj
,因此最终指向被 apply
改变的 this
指向,即 obj
func2
是箭头函数,根据箭头函数在被定义(或被创建)的时候就确认了 this 的指向(即包含箭头函数的上下文也就是外层的 this ),并且 this 是静态的 ,不管你使用 apply
还是赋值,this
的指向仍旧指向 func2
被创建时的上下文 window
。
思考:如果有多个 apply 或者赋值, this 的指向又会是什么呢?
四阶------回调函数中的普通函数和箭头函数
回调函数在函数里定义
js
setTimeout(function() {
console.log(this, 1)
}, 1000); // window 1
setTimeout(() => {
console.log(this, 2)
}, 1000); // window 2
乍一看有点懵逼,不知道怎么下手判断,但只需要把握了普通函数是谁调用的,箭头函数是在哪里创建的,即可判断出 this
的指向。
针对回调中的普通函数,得先清楚 setTimeout
这个延时的回调是谁调用的,根据 setTimeout
是宏任务,由宿主环境(即浏览器 window
)调用,所以,最后的普通函数 this
指向的是调用了回调函数的 window
。
针对回调中的箭头函数,它的创建是在回调函数中的,但一开始回调函数还没有执行,继承外层最近的普通函数的上下文 this ,所以最后回调函数中的箭头函数的 this
是指向 window
的。
相当于
js
setTimeout(fn, delay) {
console.log(this) // window
fn()
}
setTimeout(() => {
console.log(this, 2)
}, 1000);
因为 setTimeout
是在 window
中创建的,setTimeout
的上下文就是 window
,也就是说 setTimeout
{}里面的 this
是指向 window
的,而箭头函数最终会指向 setTimeout
的上下文,因此最后回调函数中的箭头函数的 this
是指向 window
的。
回调函数在函数外定义
js
function func1() {
console.log(this, 1)
}
const func2 = () => {
console.log(this, 2)
}
setTimeout(func1, 1000); // window 1
setTimeout(func2, 1000); // window 2
同上述分析类似,这里就不再多言。
对象里的回调函数
js
const obj = {
sex: 1,
setTimeout(function() {
console.log(this,this.sex, '1')
}, 1000), // window, undefined, '1'
setTimeout(() => {
console.log(this,this.sex, '2')
}, 1000) // window, undefined, '2'
}
同上述分析类似,就是多了一层对象 obj
,但不是函数,因此没有影响。因此,第一个普通函数作为回调函数 this
指向 window
,由于是 window
调用的 setTimeout
;第二个箭头函数作为回调函数 this
指向 window
,虽然 setTimeout
是在 obj
里创建的,但因为继承外层最近的普通函数的上下文 this ,而 obj
不是函数只是对象,所以往外找到了 window
。
在对象里的普通函数(箭头函数)包含回调函数
js
// 传统函数中的this绑定
const obj = {
sex: 1,
func1: function() {
setTimeout(function() {
console.log(this, this.sex, 1)
}, 1000);
},
func2: function() {
setTimeout(() => {
console.log(this, this.sex, 2)
}, 1000);
},
func3: () => {
setTimeout(function() {
console.log(this, this.sex, 3)
}, 1000);
},
func4: () => {
setTimeout(() => {
console.log(this, this.sex, 4)
}, 1000);
}
};
obj.func1();
obj.func2();
obj.func3();
obj.func4();
obj.func1()
,首先 func1
是个普通函数,而 setTimeout
里的回调函数也是普通函数,这很nice,减少了我们的判断,根据普通函数最终是谁调用 this 就指向谁 ,而 setTimeout
根据上述的回答它是由宿主环境(即 window
)调用的,所以它指向 window
((觉得不对的话可以试试在 func1
里添加一个console.log(this,this.sex, 'func1')
,看看这个输出即可)
js
func1: function() {
console.log(this, this.sex, 'func1') // 也就是添加一行这个
setTimeout(function() {
console.log(this, this.sex, 1)
}, 1000);
}
obj.func2()
,首先 func2
是个普通函数,但是,setTimeout
里的却是个箭头函数,根据箭头函数在被定义(或被创建)的时候就确认了 this 的指向 ,箭头函数在哪被创建的?是 setTimeout
啊,但 setTimeout
又是在普通函数 func2
中声明的,根据箭头函数继承外层最近的普通函数的上下文 this ,此时 func2
是个普通函数,它的上下文是 obj
(普通函数在 obj
中创建的),而 setTimeout
被 func2
包裹着,所以由里往外走,箭头函数 this
的指向是普通 func2
的上下文,即 obj
。(如果感觉还是难以接受的话,可以在 func2
里添加一个 console.log(this)
,看看这个 this
的输出即可)
js
func2: function() {
console.log(this, 'func2') // 也就是添加一行这个
setTimeout(() => {
console.log(this, this.sex, 2)
}, 1000);
},
从上述两个分析,应该隐隐约约有个结论了
分析 this 指向时,从内往外进行分析
箭头函数作为回调函数时,this 会和包裹着箭头函数的函数(setTimeout)上下文一致。
可以使用这个结论,试试分析接下来的两个嵌套
obj.func3()
,由于 setTimeout
的回调函数是普通函数,而 setTimeout
是 window
调用的,所以不管外面是箭头函数还是普通函数,this
指向 window
。
obj.func4()
,由于 setTimeout
的回调函数是箭头函数,根据箭头函数作为回调函数时,this 会和包裹着箭头函数的函数(setTimeout)上下文一致 ,所以 this
指向 func4
,但由于 func4
是箭头函数,根据箭头函数继承外层最近的普通函数的上下文 this ,所以 obj.func4()
的 this
指向 window
。
思考:为什么 func2 和 func4 同样是在 obj 中定义的,为什么 this 一个指向 obj ,一个指向 window。
js
const obj = {
sex: 1,
func2: function() {
setTimeout(() => {
console.log(this, this.sex, 2)
}, 1000);
},
func4: () => {
setTimeout(() => {
console.log(this, this.sex, 4)
}, 1000);
}
};
因为 func2
是普通函数,它有自己的上下文,里面的作为箭头函数的回调函数由于箭头函数继承外层最近的普通函数的上下文 this ,指向的是 func2
里的上下文,即 obj
,而 func4
是箭头函数,里面的回调函数本来是指向 func4
里的上下文的,但 func4
不争气,它是个箭头函数,所以上下文继续往外指,即 obj
里的上下文,而 obj
是个对象,不是函数,this
只能继续往外找了,即 window
。因此,最后 obj.func2()
里的回调函数 this
指向 obj
,obj.func4()
里的回调函数 this
指向 window
。
抽离一层
js
function fn1() {
console.log(this, this.sex, 1)
}
const fn2 = () => {
console.log(this, this.sex, 2)
}
const obj = {
sex: 1,
func1: function() {
setTimeout(fn1, 1000);
},
func2: function() {
setTimeout(fn2, 1000);
},
func3: () => {
setTimeout(fn1, 1000);
},
func4: () => {
setTimeout(fn2, 1000);
}
};
obj.func1(); // window, undefined, 1
obj.func2(); // window, undefined, 2
obj.func3(); // window, undefined, 1
obj.func4(); // window, undefined, 2
这样看可能会更清晰一点,但结果和流程实则有点区别,普通函数当然没问题,即 func1
和 func3
,都是 window
调用 setTimouet
, this
指向 window
。
obj.func2
和 obj.func4
里的 setTimeout
回调函数都是 fn2
箭头函数,根据箭头函数在被定义(或被创建)的时候就确认了 this 的指向 ,箭头函数 fn2 创建时是在 window
下创建的,所以 this
指向 window
。
自定义的回调函数
js
function someFunction(callback) {
console.log(this, '1') // window
callback();
}
// 传统回调函数
someFunction(function() {
console.log(this, '2'); // window
});
// 箭头函数作为回调函数
someFunction(() => {
console.log(this, '3'); // window
});
第一个回调函数是普通函数,由于作为普通函数的回调函数 this
是由调用方式决定的,所以可以看到调用 someFunction
的是 window
(window.someFunction(...)),因此 somwFunction
里的 this
指向的 window,'1'
。而 callback()
呢,它被谁调用了,我们上下一看,发现它没有人调用,或者说,其实它是被 window
调用的(window.callback()),即普通函数则是最终谁调用的,this 就指向谁。如果最终没有谁调用,那就是由 window 调用的 ,因此 someFunction
里的回调参数,this
指向 window , '2'
。
第二个回调函数是箭头函数,就相当于把箭头函数放入了 someFunction
函数中调用了,所以作为箭头函数的回调函数 this
是指向 someFunction
内部的,而 someFunction
是在 window
中创建的,所以 someFunction
的上下文为 window
,根据箭头函数继承外层最近的普通函数的上下文 this ,因此箭头函数里的 this
最终是指向 window
的。
下面这个就不分析,和上面的setTimeou本质是一样的。
js
function someFunction(callback) {
console.log(this, '1')
callback();
}
const obj12 = {
sex: 1,
// 传统回调函数
func1: function () {
someFunction(function () {
console.log(this, this.sex, '2'); // 外层上下文的this
})
},
// 箭头函数作为回调函数
func2: function () {
someFunction(() => {
console.log(this, this.sex, '3'); // window
})
},
func3: () => {
someFunction(function () {
console.log(this, this.sex, '2'); // 外层上下文的this
})
},
func4: () => {
someFunction(() => {
console.log(this, this.sex, '3'); // window
})
}
}
obj12.func1()
obj12.func2()
obj12.func3()
obj12.func4()
五阶------闭包中函数和箭头函数
js
let obj2 = {
func1: function () {
console.log(this, '1')
function func2() {
console.log(this, '2')
}
func2();
}
}
obj2.func1()
obj2.func1()
,func1
是个普通函数,根据普通函数则是最终谁调用的,this 就指向谁 ,所以,第一个 this
指向 obj2
,而 func2
,它没有被谁调用,相当于 window.func2()
,根据如果最终没有谁调用,那就是由 window 调用的 ,因此 func2
里的 this
指向 window
。