javascript中的this指向

一.什么是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中其实这句话在箭头函数下是不成立的。

  1. 在ES6中this的指向是固定的。而不像ES5中,指向是可变的,this指向运行时所在的作用域
  2. 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
  1. 执行b.fn(),第一个定时器因为是es5写法,所以此时this因为线程问题指向window,所以输出1。第二个定时器采用箭头函数,所以不依据线程,只会在作用域链上级寻找,fn是其上层作用域,因为b调用fn,所以fn中的this指向b,所以箭头函数中的this此时指向b,所以输出2
  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
  1. 执行obj.b.c(),因为c是箭头函数写法,所以this指向上层作用域链,作用域包括函数(局部)作用域和全局作用域。因为上层没有函数作用域,所以指向全局作用域window
  2. obj.b.d()(),相当于执行d返回的是箭头函数,所以里面的this指向上层作用域链,也就是d。因为d是es5写法,依据谁调用指向谁,此时d中的this指向b。所以输出为b

八.总结

其实this指向不难,比较麻烦的场景,只要用心去确定哪个对象调用了函数,就能很快确定this的指向

完整梳理this指向,看完保证秒懂

阮一峰-Javascript 的 this 用法

推荐阅读更多精彩内容