javascript精雕细琢(四):认亲大戏——通过console.log彻底搞清this

function下的this 2、箭头函数下的this 结语 引言 JS中的this指向一直是个老生常谈,但是新手又容易晕的地方。我在网上浏览了很多帖子,但是发现一个通病,也是博客的局限性——重理论说明,实践性低。最多就是贴个代码或者图,增加可理解性。 所以,我就想通过代码学习黄金法则——敲就完了。以console.log,循序渐进,一步步的实践,来说明this的指向。而从我自身的理解角度来讲,这个方法效果还不错~ 那么,接下来我将从两个方面——普通函数及箭头函数两个方面来说明this指向。建议将所有代码copy下来,一步步打印, 最终肯定能够理解this的指向。如果没理解,那么,再打印一遍~ 代码在前 // function下的this基于执行环境的指定。简单理解就是函数前边挂谁,this就是谁,没有就是window。那么函数直接调用和匿名函数自执行由于前边什么都没有,就指向window。 // () => {}箭头函数下的this基于作用域链查找确定,向上查找this,那个作用域里有this,就调用这个this。函数直接调用和箭头函数自执行仍旧会遵循查找原则。 //--------function下的this-------- //----首先是普通的函数声明 // function test() { // console.log(this); // } // test(); // 打印window,因为没有指明执行环境,那么执行环境就是window // //----如果是闭包呢? // function test() { // console.log(this); // const log = "Lyu"; // const fn = function() { // console.log(log); //打印Lyu // console.log(this); //打印window // } // // fn(); // 打印Lyu,window // } // test(); //window,Lyu,window。 首先调用test(),由于test()前什么都没挂,this就指向window。然后test函数内部,由于fn()前也什么都没有挂,this同样指向window。举这个例子是想证明,this并不会受函数额作用域及执行上下文影响,必须明确指定。 // //----然后是匿名函数自执行? // function test() { // (function(){ // console.log(this); // })() // } // test(); //window,因为匿名函数自执行前边就不能挂其他玩意儿,所以它始终指向window // //----接下来看明确指定执行环境的例子 // const obj = { // fn: function() { // console.log(this); // } // } // obj.fn(); //打印obj,因为指定执行环境为obj的{}块级作用域内 //--如果改变一下调用方式呢? // const fn = obj.fn; // 此时fn = function() { console.log(this) },相当于创建了一个全局函数 // fn(); //打印window,因为没有指定执行环境 // //----事件调用下的this // document.onclick = function() { // console.log(this); //打印document,因为指定执行环境为document,即document在click时触发 // } //这里相信大家都是明白的,就不再多赘述了 //----最后是通过call、apply、bind绑定下的this //一句话说明,不再举例。前边说了,function下的this,通过指定执行环境来确定的,而call、apply、bind就是用来指定执行环境的,所以指谁,this就是谁。 //--------箭头函数下的this-------- //既然箭头函数下的this通过作用域链查找,那么作用域中如果没有声明this值,那么就向上查找 //----先说明this的创建与查找 // function Test() { // console.log(this); // } // Test(); // window,未指定作用环境,所以this指向window // new Test(); //Test{},此时,构造函数内Test()内的this被new声明,this指向构造函数创建的对象Test{},所以打印Test{}对象 // //----接下来是箭头函数 // function Arrow() { // window.onmousewheel = () => { // console.log(this); // } // } // Arrow(); // window,直接调用时,Arrow()函数内并没有声明this,所以滚动鼠标,this会随作用域链查找。先在Arrow函数内,没找到this。然后一直向上,最终找到window。 //--然后我们new它一下子 // new Arrow(); //此时通过new,构造函数Arrow()内的this被声明,且指向对象Arrow{},所以箭头函数在作用域链中查找时,在Arrow函数内就找到this为Arrow{} //----接下来复杂一点,来个事件,顺便再加点匿名函数自执行 // function Go() { // //new一个this妈妈 // window.onmousewheel = () => { // console.log(this); // 妈妈不见了! // (() => { // console.log(this); // 妈妈去哪了? // (() => { // console.log(this); // 嘤嘤嘤,妈妈没了! // (() => { // console.log(this); // 走啊哥几个,找妈妈去 // })(); // })(); // })(); // } // } // Go(); //window,因为onmousewheel事件中及Go()函数中没有声明this,所以按照作用域链查找,找到window //--然后我们再new它一下子 // new Go(); // 全部打印Go{},因为new操作符,Go()函数中声明了this,且指向Go{}对象。而onmousewheel事件也用箭头函数指定,仍旧遵循查找原则。就这么一层一层的找,最后都找到Go函数作用域内的this,最后全部打印Go{}。这不就是小蝌蚪找妈妈嘛! //----接下来换个搭配方式再看一下,普通function搭配箭头函数 // function GoOn() { // document.onclick = function() { // console.log(this); // (() => { // console.log(this); // })(); // } // } // GoOn(); // document、document,此时由于function中的this已经绑定到document,所以第一个打印document; // 而由于箭头函数自执行仍旧遵循作用域链查找原则,不会指向window。所以箭头函数自执行后,根据作用域链向上查找this,找到document; //--啥也别说了,就是new它丫的 // new GoOn() // document、document,此时就算new操作声明了this,但是是在click事件外的作用域中,箭头函数在click中已经找到了this,不会再向上查找; //所以仍旧打印document,document; // //----再看个混搭,然后我们结束搭配 // function Going() { // document.onclick = function() { // console.log(this); // (function() { // console.log(this); // })(); // // (() => { // onsole.log(this); // }) // } // } // Going(); // document、window、document,首先上来就指定了this为document,所以第一个打印document; // 而function的匿名函数自执行会指向window,所以第二个打印window; // 第三个箭头函数自执行,遵循作用域链查找原则,在onclick事件中找到this为document,所以打印document; // --new、new、new // new Going(); //规则不变,结果不变 // //----好,混搭看完,接下来说个更有意思的。关于作用域的形成 //----JS的函数作用域及作用域链,是在函数创建时就被固定的 //----这么说确实不太直观,那么通过举例来说明 // function Test() { // console.log(this) // innerTest(); // } // const innerTest = () => { // console.log(this); // } // Test(); //window、window,如果不明白,把上边再看一遍 // new Test(); // Test{}、window,从这就可以看出端倪了。 // 为什么Test内的this指向Test{}对象了,而箭头函数中的this仍旧为window呢?innerTest函数在Test函数内执行,说好的按照作用域链查找呢? // 请看文章中详细的解释 // //----最后,call、apply、bind下的箭头函数 //一句话说明,箭头函数的this改不了,干啥都改不了,咋着都改不了,硬气! 1、function下的this 我将从普通的函数声明、匿名函数自执行、对象声明、事件绑定、及call等方法绑定来分别说明function下的this指向。 function下的this理解起来也简单。我们就以亲爹和干爹来比喻: 假设window是所有函数的干爹。我们是公益组织,要给JS下的函数找到它们的亲爹,而function声明的函数,都是渴望父爱的男孩; 确认亲爹的方式就是调用函数的时候在它们前边加个 .(点) 或者 ["name"],或者通过call、apply、bind其他手段确认; 而那些调用时候,前边嘛也没有的,他们亲爹没找到,那他们的干爹就当亲爹来孝顺; 这场公益认爹,就是function的this行为—函数前边有.(点)或者["name"],明确指定了亲爹的,this就是亲爹;直接调用的函数、匿名自执行的函数,这俩没找到亲爹的,this就是干爹window;而通过call、apply、bind其他渠道找到的亲爹,this同样是亲爹; 下面详细说一下这场认亲公益行, 各种情形下的this指向,一切以上边贴的代码为基础! 1) 普通function声明 最常用的函数声明无非是两种: function test() {} 及 const test = function() {} 这两种写法的区别在于声明方式的不同,进而影响变量提升,并不会对this的指向产生影响。 这两种声明方式下的function函数,在调用时,通常就是直接调用。那么通过认爹我们就能知道,这种情况下的this就是window。 function test() { console.log(this); } test(); // 打印window,因为没有指明执行环境(没找到亲爹),那么执行环境就是window(干爹) 为什么代码里我加上了闭包的说明呢?主要是为了跟箭头函数做一个区分,证明一下,function下的this跟作用域以及作用域链无关。同时跟它调用时的执行上下文也无关,就是看函数前边有没有 .(点)——必须明确它的亲爹。 2) 自执行匿名function声明函数 与函数直接调用同理,不再赘述,匿名函数自执行就理解成父母双亡,这货再也没有亲爹了,所以它的this始终指向window。 3) 对象下的function声明 对象下声明的函数,在调用时是要通过对象方法访问的,所以~肯定有爹! 但是这里边分了两种情况,一种情况是正常的通过对象调用方法,另一种跟直接调用函数无异~ const obj = { fn: function() { console.log(this); } } obj.fn(); //打印obj,因为指定执行环境为obj的{}块级作用域内(亲爹为obj) //如果改变一下调用方式呢? const fn = obj.fn; // 此时fn = function() { console.log(this) },相当于普通的function创建函数 fn(); //打印window,因为没有指定执行环境(没亲爹) 4) 事件调用下的function声明 事件的一般写法上,它必须要有 .(点)或者[name],所以它 肯定有亲爹(最幸福的function函数),那么.(点)前是谁,亲爹就是谁~ document.onclick = function() { console.log(this); //打印document,因为指定执行环境为document,即document在click时触发 } 5) call、apply、bind绑定 一句话总结:给谁,谁就是亲爹! function test() { console.log(this.say); }; const obj = {say: "我是它爹"}; const father = {say: "我也是它爹"}; const result = {say: "我也是它爹,它到底几个爹"}; test.apply(obj); // 我是他爹 test.call(father); // 我也是他爹 (test.bind(result))(); // 我也是它爹,他到底几个爹 2、箭头函数下的this 首先,不明白箭头函数的,请先自行百度或者Google,不要还没开车就出车祸了; 然后,接下来我会从正常函数声明的箭头函数、匿名函数自执行的箭头函数、对象下声明箭头函数、事件调用下的箭头函数、function与箭头函数混合双打、作用域链查找、及call等方法绑定来说明箭头函数下的this指向。 那么,同上,箭头函数也来个比喻,同样用亲爹和干爹: 设定不变,window还是干爹。但是也有一点不同——那就是箭头函数她是个拜金女,就爱找有钱(this)的干爹; 而且吧,在拜金女眼里,window这个干爹是最穷的,所以不到走投无路,不找window这个干爹。而对它的亲爹,有钱(this)才行; 而this当然就是钱啦,谁有钱这箭头函数它就找谁!调用箭头函数就是找钱! 所以啊,在这场发家致富之旅中,箭头函数中this的指向也是很明确的——如果当前作用域中,没有通过call、apply、bind、new等操作明确this的指向(没钱),那么箭头函数将沿着作用域链(关系网)继续想上查找,直到找到明确的this(有钱的干爹)为止 1) 正常声明的箭头函数 同function不同,对于箭头函数,只有一种声明方式: const arrow = () => {} 可以变换的地方就是参数和返回值部分的简写 同样的,这种声明方式下的函数,就是直接调用。那么根据前边的比喻,箭头函数的认爹方式跟function是大不相同的。直接调用箭头函数时,这个拜金女就开始见钱眼开了——它先在当前作用域中找,当前作用域下如果没有明确的this(钱),就继续沿着作用域链往上找,直到找到this为止,因为有window这个干爹保底,所以一点好处没捞到的时候,就找window。 function Arrow() { window.onmousewheel = () => { console.log(this); } } Arrow(); // window,直接调用时,Arrow()函数内并没有明确的this(没钱),所以滚动鼠标,this会随作用域链查找(这个干爹不行,就再换个干爹)。先在Arrow函数内,没找到this。然后一直向上,最终找到window(只能保底)。 new Arrow(); //此时通过new,构造函数Arrow()内的this被声明(有钱了),且指向对象Arrow{},所以箭头函数在作用域链中查找时,在Arrow函数内就找到this为Arrow{} 2) 自执行匿名箭头函数 箭头函数是个很有原则的拜金女,不管怎么执行它,它就认钱,就认钱,就认钱(重要事情说3遍),有钱才是爹。所以就算是自执行的匿名箭头函数,它仍旧遵循找爹原则,没钱免谈,我接着向上找。 所以,它仍旧先在当前作用域中找,当前作用域下如果没有明确的this(钱),就继续沿着作用域链往上找,直到找到this为止。都没有,就找window。 function Test() { console.log(this); (() => { console.log(this); })(); } Test(); //window、window; new Test(); //Test{}、Test{}; //规则同上,不再赘述 3) 作为对象方法的箭头函数 按照function声明的逻辑,对象调用它下面的方法,this肯定是指向对象的。那么箭头函数是否也是如此呢?答案肯定是否定的,因为对象中并没有明确的this,而且对象还不能new,所以这就悲催了——箭头函数所存在的对象,永远不可能是它的干爹(只限于父女关系的箭头函数与对象,不包括function与箭头函数混搭的爷孙关系等等); const obj = { test: () => { console.log(this); } fn: function() { (() => { console.log(this) }) } } obj.test() // window,obj.test内没有明确的this(钱),所以向上找到obj,结果obj也没有钱,所以最后只能委曲求全,找window obj.fn() // obj,obj.fn中由于function的存在,this指向obj,所以一发命中,直接找obj认爹 4) 事件调用下的箭头函数 其实作为一个拜金女,箭头函数的生活还是挺无趣的,规则太单一。就拿这个事件调用来说吧,还是一个套路。不管我是不是你亲生的,反正你没钱,我就不认你。 function Go() { //没想到唯一的希望也是身无分文……唉,又得window了 window.onmousewheel = () => { console.log(this); // 亲爹看来你也没钱啊! (() => { console.log(this); // 又一个穷货! (() => { console.log(this); // 这也没钱! (() => { console.log(this); // 钱呢! })(); })(); })(); } } Go(); // window,因为onmousewheel事件中及Go()函数中没有明确的this(钱),所以按照作用域链查找,找到window(走投无路) new Go(); // 有钱了 // 全部打印Go{},因为new操作符,Go()函数中声明了this,且指向Go{}对象。 // 而onmousewheel事件也用箭头函数指定,仍旧遵循查找原则。就这么一层一层的找,最后都找到Go函数作用域内的this(钱),最后全部打印Go{}(逮着一个有钱的可劲造,全造它一个)。 // Go{}对象左拥右抱,帝王生活让人向往! 5) function与箭头函数混搭及JS静态作用域 俗话说得好哇,一山不容二虎,除非一公一母!还有就是男女搭配,干活不累! function与箭头函数这一男一女遇上后,那是干柴遇烈火,一拍即合,合作起来非常愉快! 在function这个拉皮条缺父爱的男孩帮助下,箭头函数找干爹变得容易起来~ 以开头代码中挖的坑为例,顺带说一下JS中的静态作用域 function Test() { console.log(this) innerTest(); } const innerTest = () => { console.log(this); } Test(); // window、window new Test(); // Test{}、window,从这就可以看出端倪了。 按照上边一路顺下来的思路理解的话,第二次new操作之后,应该打印Test{}和Test{}对不对? 让我们捋一下思路: 在Test函数里调用innerTest函数,innerTest函数是一个箭头函数。那么我在Test里调用它的时候,这拜金女肯定是一步一步的往上找this(钱); 第一次无new直接调用没毛病,Test里没this(钱),所以找了window; 可是第二次new操作后,Test有this(钱)了,为啥箭头函数没找Test?难道嫌它丑? 一张图说明情况: 从Chrome控制台打印的作用域中可以看出,innerTest的作用域链中根本没有Test函数,所以它压根不会在Test中查找this。 这就表明了JS作用域与作用域链的一个问题——静态。即函数的作用域及作用域链,在函数声明时形成,并且保持不变。因为innerTest是在全局声明的,所以它的作用域链只有Script及Global,就算再Test函数内调用,也不会改变,除非在Test函数内再声明一个函数,那么该函数的作用域及作用域链中就包含了Test函数,不管有没有通过闭包调用Test函数中的变量(不调用Test函数内变量的话,Chrome浏览器控制台中打印不出来闭包作用域)。 6) call、apply、bind绑定 一句话总结:我对this(钱)很专一的! 箭头函数的this指向,无法通过call、apply、bind改变!贼专一! function Test() { const fn = () => { console.log(this); } fn(); // Test{} const say = function() { console.log(this); } say() // window function Replace() { console.log(this); // Replace{} fn.call(this); // Text{} say.call(this); // Replace{} } new Replace(); } new Test(); 结语 至此,这场认亲大戏就到此完毕。整体内容还是有点多的,我相信大多数人是没耐心读完的,所以我尽量想写的幽默有趣一点。就像开夜路怕困,会话会变多、抽烟解困,有的人肯定会反感这种文风,但我也没那么多读者~哈哈哈。 最后,有的点挖的还是不够深的,没办法,水平真是有限,挖不动了。如果能给到各位启发,希望你能继续挖下去~https://www.cnblogs.com/keepStudying/p/9971919.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信