目录
- 引言
- 代码在前
- 1、function下的this
- 2、箭头函数下的this
- 结语
引言
JS中的this指向一直是个老生常谈,但是新手又容易晕的地方。我在网上浏览了很多帖子,但是发现一个通病,也是博客的局限性——重理论说明,实践性低。最多就是贴个代码或者图,增加可理解性。
所以,我就想通过代码学习黄金法则——敲就完了。以console.log,循序渐进,一步步的实践,来说明this的指向。而从我自身的理解角度来讲,这个方法效果还不错~
那么,接下来我将从两个方面——普通函数及箭头函数两个方面来说明this指向。建议将所有代码copy下来,一步步打印, 最终肯定能够理解this的指向。如果没理解,那么,再打印一遍~
代码在前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148// 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声明
最常用的函数声明无非是两种:
1function test() {} 及 const test = function() {}
这两种写法的区别在于声明方式的不同,进而影响变量提升,并不会对this的指向产生影响。
这两种声明方式下的function函数,在调用时,通常就是直接调用。那么通过认爹我们就能知道,这种情况下的this就是window。
1
2
3
4function test() { console.log(this); } test(); // 打印window,因为没有指明执行环境(没找到亲爹),那么执行环境就是window(干爹)
为什么代码里我加上了闭包的说明呢?主要是为了跟箭头函数做一个区分,证明一下,function下的this跟作用域以及作用域链无关。同时跟它调用时的执行上下文也无关,就是看函数前边有没有 .(点)——必须明确它的亲爹。
2) 自执行匿名function声明函数
与函数直接调用同理,不再赘述,匿名函数自执行就理解成父母双亡,这货再也没有亲爹了,所以它的this始终指向window。
3) 对象下的function声明
对象下声明的函数,在调用时是要通过对象方法访问的,所以~肯定有爹!
但是这里边分了两种情况,一种情况是正常的通过对象调用方法,另一种跟直接调用函数无异~
1
2
3
4
5
6
7
8
9
10const 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函数),那么.(点)前是谁,亲爹就是谁~
1
2
3document.onclick = function() { console.log(this); //打印document,因为指定执行环境为document,即document在click时触发 }
5) call、apply、bind绑定
一句话总结:给谁,谁就是亲爹!
1
2
3
4
5
6
7
8
9function 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不同,对于箭头函数,只有一种声明方式:
1const arrow = () => {}
可以变换的地方就是参数和返回值部分的简写
同样的,这种声明方式下的函数,就是直接调用。那么根据前边的比喻,箭头函数的认爹方式跟function是大不相同的。直接调用箭头函数时,这个拜金女就开始见钱眼开了——它先在当前作用域中找,当前作用域下如果没有明确的this(钱),就继续沿着作用域链往上找,直到找到this为止,因为有window这个干爹保底,所以一点好处没捞到的时候,就找window。
1
2
3
4
5
6
7
8function 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。
1
2
3
4
5
6
7
8
9function Test() { console.log(this); (() => { console.log(this); })(); } Test(); //window、window; new Test(); //Test{}、Test{}; //规则同上,不再赘述
3) 作为对象方法的箭头函数
按照function声明的逻辑,对象调用它下面的方法,this肯定是指向对象的。那么箭头函数是否也是如此呢?答案肯定是否定的,因为对象中并没有明确的this,而且对象还不能new,所以这就悲催了——箭头函数所存在的对象,永远不可能是它的干爹(只限于父女关系的箭头函数与对象,不包括function与箭头函数混搭的爷孙关系等等);
1
2
3
4
5
6
7
8
9
10
11
12const 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) 事件调用下的箭头函数
其实作为一个拜金女,箭头函数的生活还是挺无趣的,规则太单一。就拿这个事件调用来说吧,还是一个套路。不管我是不是你亲生的,反正你没钱,我就不认你。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function 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中的静态作用域
1
2
3
4
5
6
7
8
9function 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改变!贼专一!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function 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
最后
以上就是繁荣狗最近收集整理的关于javascript精雕细琢(四):认亲大戏——通过console.log彻底搞清this的全部内容,更多相关javascript精雕细琢(四):认亲大戏——通过console内容请搜索靠谱客的其他文章。
发表评论 取消回复