一.什么是this
this
是 JavaScript 语言的一个关键字。它是函数运行
时,在函数体内部自动生成的一个对象,只能在函数体内部使用。
那么,this的值是什么呢?函数的不同使用场合,this有不同的值。总的来说,this就是函数运行时所在的环境对象
。
二.理解好this指向的关键要素
this调用的指向,其实取决于函数的调用方式,也就是在运行时,哪个函数内部使用了this,那么this就指向调用此函数的对象
。这样就很容易理解this指向了,看到判断this指向问题的第一反应,去找使用了this这个函数被谁调用就完事了。如,A函数中使用了this,那么就去找哪个对象调用了A函数。
三.普通的函数调用
var a = 1;
function test() {
console.log(this.a); // 1
}
test();
我们拿上面的test函数
分析下,这是函数的最通常用法,属于全局性调用,挂载在全局(window
)上的,所以函数是被window
调用的,那么这里的 this
就指向了window
,所以window.a === this.a === 1
;(注:如果严格模式,this为undefined。需要了解严格模式,可以戳这javascript严格模式
如果把函数改成这样
var a = 1;
function test() {
var a=2;
console.log(this.a); // 1
}
test();
这个也好理解,因为this始终都是指向window,那么window中a变量一直都是1,所以test函数中的a变量不会覆盖了window上的a变量。
我们再把函数变一下
var a = 1;
function test() {
a=2;
console.log(this.a); // 2
}
test();
这个如何理解呢,同理这个时候是window调用了test函数
,this
还是始终指向window
,但是test函数
中a
变量没有加var
,就被默认为全局的变量,覆盖了上一次的a
变量,所以this.a
就变成了最后一次定义a
变量的值,也就是2
四.作为对象方法的调用
函数还可以作为某个对象的方法调用,顾名思义,就是对象调用了这个函数(方法),这时this就指该对象。
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a) //2
}
}
b.fn();
即b对象调用了fn函数,那么this指向了b对象,所以this.a === b.a === 1
现在我们修改一下函数
var a = 1;
var b = {
a: 2,
c: {
a:3,
fn: function () {
console.log(this) // {a: 3, fn: ƒ}
console.log(this.a) //3
}
}
}
b.c.fn();
即b调用c,c调用fn,所以this指向直接调用这,也就是c,所以结果为3
我们再次修改一下函数
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a) //2
}
}
window.b.fn();
window调用了b对象,b对象调用了fn方法,本质上,调用fn方法的还是b对象,所以this一直指向b对象,也即输出了2,如果将b对象中的a属性去掉,那么理论上就应该输出undefined,因为this还是在b对象上,b中并没有a属性。代码如下:
var a = 1;
var b = {
// a: 2,
fn: function () {
console.log(this.a) //undefined
}
}
window.b.fn();
我们继续往下看
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a) //1
}
}
var fn1=b.fn;
fn1()
b对象将fn方法赋值给fn1变量,那么此时fn方法并没有被任何人调用,只是单纯的赋值。执行fn1方法之后,fn方法才算真正被调用,那么此时fn1挂载在全局window上,也就是window对象去调用了fn1方法,所以fn1的指向就变成了window,也就输出了1;
如果改成下面
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a) //2
}
}
var fn1=b.fn();
b对象将fn方法赋值给fn1变量并调用,所以此时仍是b对象调用,所以this仍指向b对象,也就输出了2
那么如果是这种情况呢
var a = 1;
var b = {
a : 2,
fn : function(){
setTimeout(function() {
console.log(this.a) //1
},100)
}
}
b.fn();
从上述中可以看到b.fn()里面执行了setTimeout函数,setTimeout内的this是指向了window对象,这是因为setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。熟知EventLoop的人员应该明白,setTimeout函数其实调用的是浏览器提供的其他线程,当JS主线程走完之后,会调用任务队列的函数,也即是setTimeout函数,此时setTimeout是在window上调用的,那么this自然指向了window。不了解可以看我这边文章浏览器的运行机制—3.浏览器的渲染进程
五.作为构造函数调用
所谓构造函数,就是通过这个函数,可以生成一个新对象(object)。这时,this就指这个新对象。如果不使用 new
调用,则和普通函数一样
function Name(b) {
this.b = b;
console.log(this)
}
var o = new Name;
console.log(o) //Name {b: undefined}
var o = new Name(1); // 调用函数,输出结果为:Name {b: 1"}
function Name(b) {
this.b = b;
console.log(this) //Window
}
Name(1)
可以看出如果是通过new
生成的对象,this是指向生成的对象。
如果改成下面
function Other(){
this.ff=0
}
Other.prototype={
ff:2,
get:function(){
console.log(this.ff) //0
}
}
var mm=new Other
console.log(mm) //Other {ff: 0}
mm.get()
上面例子,实例mm通过new生成,所以构造函数中的this指向生成的对象mm。
然后mm调用get方法,所以this指向调用者mm,因为mm对象含有ff属性,所以结果为0
六.使用 apply 或 call 调用
apply、call
函数都可以用来更改this指向
,具体两者的用法我会专门开一篇文章来讲解。因为apply与call函数在使用上,基本只有传入参数不同的区别(apply第二个参数是数组,call第二个参数不为数组),本文拿call来讲解。我们直接看代码
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a)
}
}
var c = {
a: 3
}
b.fn(); //2
b.fn.call(window); //1
b.fn.call(c); //3
从代码可以看出,call可以直接把this指向作为参数传入。我们通过结果可以得出call可以改变this指向,那么问题来了,call函数是怎么改变this指向的呢,我们可以看一下call的简单实现
Function.prototype.call = function(context) {
context.fn1 = this; //因为this是指调用call的函数,所以this就是fn,我们往传过来的context定义一个方法fn1等于this(该函数fn),然后调用,此时该函数里面的this就指向context了,最后我们删除该方法即可。
context.fn1();
delete context.fn1;
}
七.箭头函数的this指向(ES6)
在es5
中,this的指向可以简单的归结为一句话:this指向函数的调用者。那么在ES6
中其实这句话在箭头函数下是不成立的。
- 在ES6中this的指向是固定的。而不像ES5中,指向是可变的,this指向运行时所在的作用域
- this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它只会从自己的
作用域链
的上一层继承this。正是因为它没有this,所以也就不能用作构造函数,并且也就不能用call()、apply()、bind()这些方法去改变this的指向(改变无效,输出的仍是作用域链的上一层)
1.箭头函数只会从自己的作用域链的上一层继承this
我们来看看这个例子:
var a = 1;
var b = {
a: 2,
fn: function () {
console.log(this.a) //2
}
}
b.fn();
现在我们改成箭头函数:
var a = 1;
var b = {
a: 2,
fn:()=>{
console.log(this.a); //1
}
}
b.fn();
上述例子中,箭头函数输出1
是因为其会从自己的作用域链
(函数作用域)上一层,也就是全局作用域继承this(这里相当于window)
2.箭头函数不能用call()、apply()、bind()这些方法去改变this的指向
我们对上面的例子进行修改:
var a = 1;
var b = {
a: 2,
fn:()=>{
console.log(this.a); //1
}
}
b.fn.call({a:3});
上面我们使用call,对箭头函数fn进行更改指向,但是this此时仍指向window。
现在我们将fn改成es5写法(非箭头函数)
var a = 1;
var b = {
a: 2,
fn: function () {
setTimeout(function(){
console.log(this.a);
},10);
setTimeout(() => {
console.log(this.a);
},10);
}
}
b.fn() //1 2
b.fn.call({a: 3}); //1 3
- 执行
b.fn()
,第一个定时器因为是es5写法,所以此时this因为线程问题指向window,所以输出1
。第二个定时器采用箭头函数,所以不依据线程,只会在作用域链上级寻找,fn是其上层作用域,因为b调用fn,所以fn中的this指向b,所以箭头函数中的this此时指向b,所以输出2
。 - 执行
b.fn.call({a: 3})
,第一个定时器因为是es5写法,所以此时this因为线程问题指向window,所以输出1
。第二个定时器采用箭头函数,所以不依据线程,只会在作用域链上级寻找,fn是其上层作用域,因为b调用fn并使用call进行指向更改,所以fn中的this指向{a:3},所以箭头函数中的this此时指向{a:3},所以输出3
。
3.多层对象嵌套里函数的this
直接上例子
const obj = {
a: function () {
console.log(this)
},
b: {
c: function () {
console.log(this)
}
}
}
obj.a() // obj
obj.b.c() //obj.b
上面的例子很好理解,因为是es5写法,所以谁调用指向谁
,结果显然易见,我就不解释了。
接着我们用箭头函数改写例子
const obj = {
a: function () {
console.log(this)
},
b: {
c: ()=>{
console.log(this);
},
d:function(){
return ()=>{
console.log(this);
}
}
}
}
obj.b.c() //window
obj.b.d()() //b
- 执行
obj.b.c()
,因为c
是箭头函数写法,所以this指向上层作用域链,作用域包括函数(局部)作用域和全局作用域。因为上层没有函数作用域,所以指向全局作用域window - obj.b.d()(),相当于执行
d
返回的是箭头函数,所以里面的this指向上层作用域链,也就是d
。因为d
是es5写法,依据谁调用指向谁
,此时d
中的this
指向b。所以输出为b
八.总结
其实this指向不难,比较麻烦的场景,只要用心去确定哪个对象调用了函数,就能很快确定this的指向