1.前言
对于前端开发人员来说,js中的this指向一直是一个非常难以搞懂的东西,若你没有花一定的时间去理解它,在开发的过程中还真的会踩很多坑。所以,我也借此博客来分享一下自己对于js中this的理解。
2.规则总结
对于js中的this到底指向什么,我借用我在别人博客上看见的几点总结。因为我看那篇文章离现在的时间比较久远,所以目前也找不到那篇文章了。只能对原作者说一声抱歉,如果有人在其他文章上看见了相似的总结或者原作者看见了这篇文章可以联系我把文章链接加上。好了,话不多说,this的取值应该要符合下面六条规则:
- 在调用函数时使用new关键字,函数中的this是一个全新的对象。
- 如果使用了apply,bind,call方法调用函数,函数中的this指向调用时传入的对象。
- 当函数是使用 . 来调用,即作为对象里的方法来调用时,函数内的this指向该对象。比如obj.method()被调用时,method方法里面的this指向obj对象。
- 如果调用函数不符合上述规则,则指向全局对象。在浏览器中应该指向window,node中指向global。但是在严格模式下this会指向undefined。
- 如果符合多条,则1的权重最高,4权重最低。
- 如果是es6中的箭头函数,忽略上诉所有规则,this值被设置为创建时的上下文。
下面我们分别解析这几条规则。
3.规则解析
- 在调用函数时使用new关键字,函数中的this是一个全新的对象。
想要理解这条规则,我想我们应该需要知道当你new一个函数的时候到底发生了什么事情。其实,在我们使用new函数时,相当于执行了下面几个步骤
我们着重关注一下第三步,在这一步中我们在调用f函数时使用了call,即把f函数的this指向指向了obj对象,所以在创建出的新实例中,this也就变成了一个全新的对象。let temp = new f(); //1. 创建一个空对象 let obj = {}; //2. 改变原型链 Object.setPrototypeOf(obj, f.prototype); //3. 使用新对象调用函数,函数中的this被指向新实例对象 f.call(obj); //4. 将初始化完毕的新对象的地址,保存到左边的变量中
- 如果使用了apply,bind,call方法调用函数,函数中的this指向调用时传入的对象
这条规则应该很好理解,因为这三个函数的作用本来就是改变this指向,所以他们的this肯定会指向调用时传入的函数。不过值得注意的是,如果你连续使用bind来改变this指向,其实只有第一个会生效,具体原因可以阅读我的另一篇博客:js中的bind、apply、call的原生实现及其思考。 - 当函数是使用 . 来调用,即作为对象里的方法来调用时,函数内的this指向该对象。比如obj.method()被调用时,method方法里面的this指向obj对象。
这个应该是我们平时遇见的最多的一种情况了,其实他也很好理解,你只需要看 . 前面是谁,this就指向谁。比如:
我们直接使用obj.f去调用函数时,会输出obj,把他赋给另一个变量再去调用时,则会输出global。因为第一种情况f是作为obj对象里的方法来调用的。但是你把这个函数赋给另一个变量,再去调用时,这是就不是作为对象里的函数去调用了,所以this指向了全局变量,即输出global。总结起来就是一句话,谁调用的就指向谁。我们再来看下面这个例子来加深一下理解:global.name = "global"; let obj = { name:"obj", f:function () { console.log(this.name) } } let f = obj.f; obj.f(); //obj f(); //global
乍一看,不是obj1调用了f么,为什么还是会输出obj2?因为f:obj1.f相当于我们前面的f = obj.f,我们把obj1.f的地址赋给了obj2的f对象,所以再调用obj2.f时,f函数是作为obj2对象里的方法来调用的,所以会输出obj2。那么我们想让他输出obj1应该如何做?let obj1 = { name: "obj1", f: function () { console.log(this.name); } } let obj2 = { name: "obj2", f: obj1.f } obj2.f(); //obj2
我们不直接把obj1的f函数赋给obj2的f,而是创建一个新函数,在里面调用obj1.f,这样就会输出obj1了,相信大家仔细想一想就能想明白其中的缘由。let obj1 = { name: "obj1", f: function () { console.log(this.name); } } let obj2 = { name: "obj2", f: function () { obj1.f(); } } obj2.f(); //obj1
- 如果调用函数不符合上述规则,则指向全局对象。在浏览器中应该指向window,node中指向global。但是在严格模式下this会指向undefined
这种情况即函数没有被谁调用,也没有使用new或者call、bind等方法的时候。最常见的即我们声明了一个函数,然后使用了这个函数:
这是最常见的情况,那么我们再来看下面这段代码:function f() { console.log(this); } f(); // 输出global对象
???,这个this不是应该是全局的this么,为什么指向了f里面的name。其实这个this还是全局的this,这里有个大坑,你相当于重新给全局name赋值,把之前定义的name给替换掉了,所以看起来像是this没有指向全局,实际上输出的还是全局的name,如果还是不理解你把name去掉直接输出this就能看出来了。最后我们再来看一个例子:this.name = "global"; function f() { this.name = "f"; console.log(this.name) } f(); //f
其中第一个this因为是obj调用的,指向了obj,所以输出为obj。而第二个没有谁调用他,符合第四条规则,所以应该指向全局,输出global。global.name = "global"; let obj = { name:"obj", f:function () { console.log(this.name); function f() { console.log(this.name); } f(); } } obj.f(); //obj //global
- 如果符合多条,则1的权重最高,4权重最低
这条规则没什么好说的,当符合多种情况时按照规则的次序来判断就行了,1权重最高,4权重最低。 - 如果是es6中的箭头函数,忽略上诉所有规则,this值被设置为创建时的上下文
注意,这是一条非常重要的规则,即es6中的箭头函数,他比较特殊,他不绑定自己的this,而是根据创建时的上下文来确定this值。我们使用第四条规则中出现过的例子,把其中一个函数改成箭头函数:
还记得之前的输出么?obj和global,我们换成箭头函数之后变成了obj和obj。因为箭头函数的this值是根据创建时的上下文确定的,所以他的this值也是obj,自然输出也就变成obj了。箭头函数的这种特性非常适合于我们在不希望改变一个函数的this值时使用。我们来看下面这个场景,我们希望点击按钮1s后输出obj对象的count值,count值会根据点击的次数不断增加,我们先看下面的代码:global.name = "global"; let obj = { name:"obj", f:function () { console.log(this.name); // function f() { // console.log(this.name); // } let f = () => { console.log(this.name); } f(); } } obj.f(); // obj // obj
点击多次后输出仍然为0:<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <button id="btn">点击我</button> <script> let btn = document.querySelector("#btn"); let obj = { count: 0, click: function () { setTimeout(function () { console.log(this.count); this.count++; alert(obj.count); }, 1000); } } btn.onclick = function () { obj.click(); } </script> </body> </html>
这种情况下我们不论点击多少次输出都会是0,因为this值根本没有指向我们想要的那个对象。那么有没有什么办法可以解决这个问题呢?我们只要把this值指向正确的对象就行了。我们分析一下,因为我们在setTimeout内部又调用了一个新函数,符合我们第四条规则,所以他的this值应该会指向全局,而setTimeout外面的this值才是正确的this值,所以我们用that变量保存下this值然后在setTimeout里面使用,把代码改成下面这样:
此时我们再来试一下,发现已经可以达到我们想要的效果了,多次点击后count值会增加:<script> let btn = document.querySelector("#btn"); let obj = { count: 0, click: function () { let that = this; setTimeout(function () { console.log(that.count); that.count++; alert(obj.count); }, 1000); } } btn.onclick = function () { obj.click(); } </script>
这里我们使用了that来保存正确的this变量,那么还有没有其他的方法?对!我们可以使用箭头函数,还记得箭头函数的特性么,this值根据创建时的上下文绑定,也就意味着他的this值就指向了obj,不需要我们做什么额外的操作。我们把代码改成下面这样:
这样很轻松就达到了我们想要的效果:<script> let btn = document.querySelector("#btn"); let obj = { count: 0, click: function () { setTimeout(() => { console.log(this.count); this.count++; alert(obj.count); }, 1000); } } btn.onclick = function () { obj.click(); } </script>
4.结语
好了,至此六种规则都已经解析完了。要想明确快速的判断this的指向还需要大家多写代码,多去运用,才能正真的做到又快又准的去判断this到底指向什么。同时本人也是一个小菜鸡,如果文中有什么说的不对的地方还请大家指出,大家一起交流学习,共同进步。