大家都知道JS中的this关键字通常出现在函数或者方法中,用来指向调用该函数或者方法的对象。但是在很多时候this的指向却并不总是如我们所愿,这一篇文章就一起来看看到底该如何判断this所指向的对象,同时在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所在的函数,回到上面的例子就是让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方法的进阶用法,就是固定函数传递的一部分参数值,有一点类似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
注意这里只能是按照参数的先后顺序进行默认值传递,例如这里就不能跨过age
给name
传递默认值。
JS中的this使用起来并不像其他OOP语言中的类似关键字方便(例如python中的self),因为有指代丢失的问题出现,只能是在实际使用的时候多多练习,熟能生巧了。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。