Javascript中this指向丢失原因及解决办法详解

大家都知道JS中的this关键字通常出现在函数或者方法中,用来指向调用该函数或者方法的对象。但是在很多时候this的指向却并不总是如我们所愿,这一篇文章就一起来看看到底该如何判断this所指向的对象,同时在this指向丢失情况下如何恢复。

文章目录

    • this指向丢失
    • 多重调用以及箭头函数
    • this指向丢失解决办法
    • bind传递函数参数
    • 总结

this指向丢失

相信有过面向对象编程经验的朋友对于this的使用不会陌生,来看两个例子

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
    }
}
let zhangsan=new Student('zhangsan');
zhangsan.sayHello(); //Hello, my name is zhangsan

这里的this指向的是构造函数生成的对象zhangsan,对象调用自身的方法sayHello(),其中的this自然不会有什么指向问题。

class People{
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    sayHello(){
        console.log(`Hello, my name is ${this.name}, I am ${this.age} years old!`);
    }
}

let xiaofu=new People('xiaofu',99);
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old!

这里只是把构造函数换成了class语法的方式,this指向的是类实例xiaofu,实例调用自身的方法,其中的this也不会有什么指向问题。

但是再看下面这个例子

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
    }
}
let zhangsan=new Student('zhangsan');
// zhangsan.sayHello();
setTimeout(zhangsan.sayHello,2000); //Hello, my name is 

注意setTimeout的第一个参数是一个函数名,而并不是具体的函数调用。所以这里并不能直接传递zhangsan,sayHello(),不然会马上执行

本意是想等待2秒之后再打印,结果打印完发现this.name并没有打印出来,this指向丢失了。按照this指向调用函数的对象的逻辑,说明2秒后调用sayHello()这个方法的已经不是zhangsan这个对象了。

如果在方法中打印一下this,就会发现此时this指向的是Window。也就是说最后一句可以像如下改写

let f=zhangsan.sayHello;
setTimeout(f,2000);

执行异步操作的时候是将一个函数丢给浏览器,2秒以后,浏览器去直接执行该函数。

这时候可以引出一个重要的结论:包含this的函数无法在定义的时候,而只有在被真正执行的时候才能知道this指向哪个对象

再看下面的例子就很容易理解了

function runFunc(func){
    this.name='lisi';
    this.sayHello=func;
}
let lisi=new runFunc(zhangsan.sayHello);
lisi.sayHello(); //Hello, my name is lisi

因为sayHello这个方法真正执行的时候是被lisi这个对象调用,所以this指向的是lisi这个对象,this.name打印了出来也是lisi。

多重调用以及箭头函数

可能有朋友又要问了,那我直接执行zhangsan.sayHello()不也是相当于在Window中去执行这个函数吗?

让我们再看下面这个例子

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
    }
    this.info={
        name:'Nobody',
        sayHello:function(){console.log(this.name)}
    }
}
let zhangsan=new Student('zhangsan');
zhangsan.info.sayHello(); //Nobody

这里调用的sayHello函数是info对象下的,可以看到函数中的this指向的是info对象,而并不是zhangsan对象。这里又可以引出另外一个重要的结论:多重调用下,函数中的this只会指向函数的上一级对象。这里函数的上一级对象是info,所以虽然zhangsan中也有一个name,但是并不会被引用。

但是这里需要注意的是箭头函数。

箭头函数在ES6中被引入,写起来简洁明了,但是有一个特点需要注意,就是箭头函数没有独立的this,其中的this会自动从上一级继承

所以如果改写下上面的代码

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
        // console.log(this);
    }
    this.info={
        name:'Nobody',
        // sayHello:function(){console.log(this.name)}
        sayHello:()=>{console.log(this.name)},
        test:this.name
    }
}
let zhangsan=new Student('zhangsan');
zhangsan.info.sayHello(); //zhangsan
console.log(zhangsan.info.test); //zhangsan

可以看出,箭头函数中使用this就和直接在info中使用this效果一样,都是指向zhangsan对象。

this指向丢失解决办法

再把话题回到this丢失上面来。

想要恢复this指向,根本逻辑就是想办法还是让this定义时候的对象来调用this所在的函数,回到上面的例子就是让zhangsan来调用sayHello()

有两种方式可以来实现,第一种是多添加一层函数调用

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
    }
}
let zhangsan=new Student('zhangsan');
// zhangsan.sayHello();
setTimeout(function(){zhangsan.sayHello();},2000); //Hello, my name is zhangsan

这里的最后一句相当于Window.zhangsan,sayHello(),根据上面的规则,this指向的是上一级对象,也就是zhangsan,所以可以成功打印出来。

并且这里使用箭头函数同样有效果,因为这里的函数只是起到多加一层包装的作用,并没有实际作用。

第二种方式是利用函数的bind方法,使用语法如下

let boundFunc = func.bind(context);

这里就是将函数func绑定到了context这个上下文上,返回一个新的函数。不管被谁调用,这个新的函数里面的this永远指向context

function Student(name){
    this.name=name;
    this.sayHello=function(){
        console.log(`Hello, my name is ${this.name}`)
    }
}
let zhangsan=new Student('zhangsan');
// zhangsan.sayHello();
setTimeout(zhangsan.sayHello.bind(zhangsan),2000); //Hello, my name is zhangsan

这里就是将sayHello()这个方法绑定到了zhangsan这个对象上,以后不管这个返回的新函数被谁调用,都可以成功返回zhangsan中的this.name

但是这里要注意的是,只能绑定到构造函数返回的具体对象上,而不能直接绑定到类名Student上。

同时要注意bind并不支持级联操作

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f(); //John

这里首先将函数f绑定到一个对象,然后马上级联操作绑定到另一个对象,可以看出只有第一个bind起了效果。

同时这里也可以看到只有在函数执行的时候才会将this指向具体的对象

bind传递函数参数

这里再提一个bind方法的进阶用法,就是固定函数传递的一部分参数值,有一点类似python中的partial函数。因为bind方法除了第一个参数是上下文,后面还可以接函数的默认参数值

function Student(name){
    this.name=name;
    this.sayHello=function(age){
        console.log(`Hello, my name is ${this.name}, I am ${age} years old.`)
    }
}
let zhangsan=new Student('zhangsan');
setTimeout(zhangsan.sayHello.bind(zhangsan,99),2000);

这里修改了sayHello()方法,必须要传递一个参数,如果想以后每次执行该方法的时候都是传递参数99就可以像上面那样。

下面来一个更通用的例子。

有一个需要传递两个参数的函数如下

function intro(age,name){
    console.log(`Hello, my name is ${name}, I am ${age} years old`);
}

通过bind方法将第一个参数值默认为99,并返回一个新函数。这里因为没有context需要传递,所以第一个参数放null,不能省略

intro99=intro.bind(null,99);
intro99('xiaofu'); //Hello, my name is xiaofu, I am 99 years old

注意这里只能是按照参数的先后顺序进行默认值传递,例如这里就不能跨过agename传递默认值。

总结

JS中的this使用起来并不像其他OOP语言中的类似关键字方便(例如python中的self),因为有指代丢失的问题出现,只能是在实际使用的时候多多练习,熟能生巧了。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

你可能感兴趣的:(前端)