大家都知道 JS 中的 this 关键字通常出现在函数或者方法中,用来指向调用该函数或者方法的对象。但是在很多时候 this 的指向却并不总是如我们所愿,这一篇文章就一起来看看到底该如何判断 this 所指向的对象,同时在 this 指向丢失情况下如何恢复。
this指向丢失
相信有过面向对象编程经验的朋友对于 this 的使用不会陌生,来看两个例子
1 2 3 4 5 6 7 8 9 10 11 12 |
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 自然不会有什么指向问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 也不会有什么指向问题。
但是再看下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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。也就是说最后一句可以像如下改写
1 2 3 |
let f = zhangsan.sayHello; setTimeout(f, 2000); |
执行异步操作的时候是将一个函数丢给浏览器,2 秒以后,浏览器去直接执行该函数。
这时候可以引出一个重要的结论:包含 this 的函数无法在定义的时候,而只有在被真正执行的时候才能知道this指向哪个对象。
再看下面的例子就很容易理解了
1 2 3 4 5 6 7 8 9 10 11 |
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 中去执行这个函数吗?
让我们再看下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 会自动从上一级继承。
所以如果改写下上面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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()。
有两种方式可以来实现,第一种是多添加一层函数调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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,所以可以成功打印出来。
并且这里使用箭头函数同样有效果,因为这里的函数只是起到多加一层包装的作用,并没有实际作用。
这里要特别说明一下特殊情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Student { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, my name is ${this.name}`); } delaySayHello() { setTimeout(() => { this.sayHello(); }, 2000); //Hello, my name is zhangsan } } let zhangsan = new Student("zhangsan"); zhangsan.delaySayHello(); |
这个情况不是很好理解,为什么箭头函数包装的 this 就可以传递过去?
其实,更方便理解的等价的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Student { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, my name is ${this.name}`); } delaySayHello() { let keepThis = this; setTimeout(() => { keepThis.sayHello(); }, 2000); //Hello, my name is zhangsan } } let zhangsan = new Student("zhangsan"); zhangsan.delaySayHello(); |
注意如下函数:
1 2 3 4 |
delaySayHello() { let keepThis = this setTimeout(() => { keepThis.sayHello(); }, 2000); //Hello, my name is zhangsan } |
这样修改会更方便理解。其实本质上箭头函数本质上就是一个对象,这个对象保持了对外部对象的引用,因此不会出现 this 丢失的情况。
第二种方式是利用函数的 bind 方法,使用语法如下
1 |
let boundFunc = func.bind(context); |
这里就是将函数 func 绑定到了 context 这个上下文上,返回一个新的函数。不管被谁调用,这个新的函数里面的this 永远指向 context。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 并不支持级联操作
1 2 3 4 5 6 7 |
function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); f(); //John |
这里首先将函数f绑定到一个对象,然后马上级联操作绑定到另一个对象,可以看出只有第一个 bind 起了效果。
同时这里也可以看到只有在函数执行的时候才会将 this 指向具体的对象
bind传递函数参数
这里再提一个 bind 方法的进阶用法,就是固定函数传递的一部分参数值,有一点类似 python 中的 partial 函数。因为 bind 方法除了第一个参数是上下文,后面还可以接函数的默认参数值
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 就可以像上面那样。
下面来一个更通用的例子。
有一个需要传递两个参数的函数如下
1 2 3 |
function intro(age, name) { console.log(`Hello, my name is ${name}, I am ${age} years old`); } |
通过 bind 方法将第一个参数值默认为 99,并返回一个新函数。这里因为没有 context 需要传递,所以第一个参数放 null,不能省略
1 2 3 |
intro99 = intro.bind(null, 99); intro99('xiaofu'); //Hello, my name is xiaofu, I am 99 years old |
注意这里只能是按照参数的先后顺序进行默认值传递,例如这里就不能跨过 age 给 name 传递默认值。
总结
JS 中的 this 使用起来并不像其他 OOP 语言中的类似关键字方便(例如 python 中的 self),因为有指代丢失的问题出现,只能是在实际使用的时候多多练习,熟能生巧了。