當一個函數調用時,會創建一個執行上下文,這個上下文包括函數調用的一些信息(調用棧,傳入參數,調用方式),this就指向這個執行上下文。
this不是靜態的,也並不是在編寫的時候綁定的,而是在運行時綁定的。它的綁定和函數聲明的位置沒有關係,只取決於函數調用的方式。
本篇文章有點長,涉及到很多道面試題,有難有簡單,如果能耐心的通讀一編,我相信以後this都不成問題。
學習this之前,建議先學習以下知識:
在文章的最開始,陳列一下本篇文章涉及的內容,保證讓大家不虛此行。
在JavaScript中,要想完全理解this,首先要理解this的綁定規則,this的綁定規則一共有5種:
下面來一一介紹以下this的綁定規則。
1.默認綁定默認綁定通常是指函數獨立調用,不涉及其他綁定規則。非嚴格模式下,this指向window,嚴格模式下,this指向undefined。
題目1.1:非嚴格模式varfoo=123;functionprint(){this.foo=234;console.log(this);//windowconsole.log(foo);//234}print();非嚴格模式,print()為默認綁定,this指向window,所以打印window和234。
這個foo值可以說道兩句:如果學習過預編譯的知識,在預編譯過程中,foo和print函數會存放在全局GO中(即window對象上),所以上述代碼就類似下面這樣:
window.foo=123functionprint(){this.foo=234;console.log(this);console.log(window.foo);}window.print()題目1.2:嚴格模式把題目1.1稍作修改,看看嚴格模式下的執行結果。
"use strict"可以開啟嚴格模式
"usestrict";varfoo=123;functionprint(){console.log('printthisis',this);console.log(window.foo)console.log(this.foo);}console.log('globalthisis',this);print();注意事項:開啟嚴格模式後,函數內部this指向undefined,但全局對象window不會受影響
答案
globalthisisWindow{...}printthisisundefined123UncaughtTypeError:Cannotreadproperty'foo'ofundefined題目1.3:let/constleta=1;constb=2;varc=3;functionprint(){console.log(this.a);console.log(this.b);console.log(this.c);}print();console.log(this.a);let/const定義的變量存在暫時性死區,而且不會掛載到window對象上,因此print中是無法獲取到a和b的。
答案
undefinedundefined3undefined題目1.4:對象內執行a=1;functionfoo(){console.log(this.a);}constobj={a:10,bar(){foo();//1}}obj.bar();foo雖然在obj的bar函數中,但foo函數仍然是獨立運行的,foo中的this依舊指向window對象。
題目1.5:函數內執行vara=1functionouter(){vara=2functioninner(){console.log(this.a)//1}inner()}outer()這個題與題目1.4類似,但要注意,不要把它看成閉包問題
題目1.6:自執行函數a=1;(function(){console.log(this);console.log(this.a)}())functionbar(){b=2;(function(){console.log(this);console.log(this.b)}())}bar();默認情況下,自執行函數的this指向window
自執行函數隻要執行到就會運行,並且只會運行一次,this指向window。
答案
Window{...}1Window{...}2//b是implyglobal,會掛載到window上2.隱式綁定函數的調用是在某個對象上觸發的,即調用位置存在上下文對象,通俗點說就是**XXX.func()**這種調用模式。
此時func的this指向XXX,但如果存在鏈式調用,例如XXX.YYY.ZZZ.func,記住一個原則:this永遠指向最後調用它的那個對象。
題目2.1:隱式綁定vara=1;functionfoo(){console.log(this.a);}//對象簡寫,等同於{a:2,foo:foo}varobj={a:2,foo}foo();obj.foo();答案
12obj是通過var定義的,obj會掛載到window之上的,obj.foo()就相當於window.obj.foo(),這也印證了this永遠指向最後調用它的那個對象規則。
題目2.2:對象鏈式調用感覺上面總是空談鏈式調用的情況,下面直接來看一個例題:
varobj1={a:1,obj2:{a:2,foo(){console.log(this.a)}}}obj1.obj2.foo()//23.隱式綁定的丟失隱式綁定可是個調皮的東西,一不小心它就會發生綁定的丟失。一般會有兩種常見的丟失:
隱式綁定丟失之後,this的指向會啟用默認綁定。
具體來看題目:
題目3.1:取函數別名a=1varobj={a:2,foo(){console.log(this.a)}}varfoo=obj.foo;obj.foo();foo();JavaScript對於引用類型,其地址指針存放在棧內存中,真正的本體是存放在堆內存中的。
上面將obj.foo賦值給foo,就是將foo也指向了obj.foo所指向的堆內存,此後再執行foo,相當於直接執行的堆內存的函數,與obj無關,foo為默認綁定。籠統的記,只要fn前面什麼都沒有,肯定不是隱式綁定。
答案
21不要把這裡理解成window.foo執行,如果foo為let/const定義,foo不會掛載到window上,但不會影響最後的打印結果
題目3.2:取函數別名如果取函數別名沒有發生在全局,而是發生在對象之中,又會是怎樣的結果呢?
varobj={a:1,foo(){console.log(this.a)}};vara=2;varfoo=obj.foo;varobj2={a:3,foo:obj.foo}obj.foo();foo();obj2.foo();obj2.foo指向了obj.foo的堆內存,此後執行與obj無關(除非使用call/apply改變this指向)
答案
123題目3.3:函數作為參數傳遞functionfoo(){console.log(this.a)}functiondoFoo(fn){console.log(this)fn()}varobj={a:1,foo}vara=2doFoo(obj.foo)用函數預編譯的知識來解答這個問題:函數預編譯四部曲前兩步分別是:
obj.foo作為實參,在預編譯時將其值賦值給形參fn,是將obj.foo指向的地址賦給了fn,此後fn執行不會與obj產生任何關係。fn為默認綁定。
答案
Window{…}2題目3.4:函數作為參數傳遞將上面的題略作修改,doFoo不在window上執行,改為在obj2中執行
functionfoo(){console.log(this.a)}functiondoFoo(fn){console.log(this)fn()}varobj={a:1,foo}vara=2varobj2={a:3,doFoo}obj2.doFoo(obj.foo)答案
{a:3,doFoo:ƒ}2題目3.5:回調函數下面這個題目我們寫代碼時會經常遇到:
varname='zcxiaobao';functionintroduce(){console.log('Hello,Mynameis',this.name);}constTom={name:'TOM',introduce:function(){setTimeout(function(){console.log(this)console.log('Hello,Mynameis',this.name);})}}constMary={name:'Mary',introduce}constLisa={name:'Lisa',introduce}Tom.introduce();setTimeout(Mary.introduce,100);setTimeout(function(){Lisa.introduce();},200);setTimeout是異步調用的,只有當滿足條件並且同步代碼執行完畢後,才會執行它的回調函數。
答案
Window{…}Hello,MynameiszcxiaobaoHello,MynameiszcxiaobaoHello,MynameisLisa所以如果我們想在setTimeout或setInterval中使用外界的this,需要提前存儲一下,避免this的丟失。
constTom={name:'TOM',introduce:function(){_self=thissetTimeout(function(){console.log('Hello,Mynameis',_self.name);})}}Tom.introduce()題目3.6:隱式綁定丟失綜合題name='javascript';letobj={name:'obj',A(){this.name+='this';console.log(this.name)},B(f){this.name+='this';f();},C(){setTimeout(function(){console.log(this.name);},1000);}}leta=obj.A;a();obj.B(function(){console.log(this.name);});obj.C();console.log(name);本題目不做解析,具體可以參照上面的題目。
答案
javascriptthisjavascriptthisjavascriptthisundefined4.顯式綁定顯式綁定比較好理解,就是通過call()、apply()、bind()等方法,強行改變this指向。
上面的方法雖然都可以改變this指向,但使用起來略有差別:
答案
211題目4.2:隱式綁定丟失題目3.4發生隱式綁定的丟失,如下代碼:我們可不可以通過顯式綁定來修正這個問題。
functionfoo(){console.log(this.a)}functiondoFoo(fn){console.log(this)fn()}varobj={a:1,foo}vara=2doFoo(obj.foo)大功告成。
題目4.3:回調函數與call接着上一個題目的風格,稍微變點花樣:
varobj1={a:1}varobj2={a:2,bar:function(){console.log(this.a)},foo:function(){setTimeout(function(){console.log(this)console.log(this.a)}.call(obj1),0)}}vara=3obj2.bar()obj2.foo()乍一看上去,這個題看起來有些莫名其妙,setTimeout那是傳了個什麼東西?
做題之前,先了解一下setTimeout的內部機制:(關於異步的執行順序,可以參考JavaScript之EventLoop[6])
setTimeout(fn){if(回調條件滿足)(fn)}這樣一看,本題就清楚多了,類似題目4.2,修正了回調函數內fn的this指向。
答案
2{a:1}1題目4.4:注意call位置functionfoo(){console.log(this.a)}varobj={a:1}vara=2foo()foo.call(obj)foo().call(obj)答案
212UncaughtTypeError:Cannotreadproperty'call'ofundefined題目4.5:注意call位置(2)上面由於foo沒有返回函數,無法執行call函數報錯,因此修改一下foo函數,讓它返回一個函數。
functionfoo(){console.log(this.a)returnfunction(){console.log(this.a)}}varobj={a:1}vara=2foo()foo.call(obj)foo().call(obj)這裡千萬注意:最後一個foo().call(obj)有兩個函數執行,會打印2個值。
答案
2121題目4.6:bind將上面的call全部換做bind函數,又會怎樣那?
call是會立即執行函數,bind會返回一個新函數,但不會執行函數
functionfoo(){console.log(this.a)returnfunction(){console.log(this.a)}}varobj={a:1}vara=2foo()foo.bind(obj)foo().bind(obj)首先我們要先確定,最後會輸出幾個值?bind不會執行函數,因此只有兩個foo()會打印a。
答案
22題目4.7:外層this與內層this做到這裡,不由產生了一些疑問:如果使用call、bind等修改了外層函數的this,那內層函數的this會受影響嗎?(注意區別箭頭函數)
functionfoo(){console.log(this.a)returnfunction(){console.log(this.a)}}varobj={a:1}vara=2foo.call(obj)()foo.call(obj): 第一層函數foo通過call將this指向obj,打印1;第二層函數為匿名函數,默認綁定,打印2。
答案
12題目4.8:對象中的call把上面的代碼移植到對象中,看看會發生怎樣的變化?
varobj={a:'obj',foo:function(){console.log('foo:',this.a)returnfunction(){console.log('inner:',this.a)}}}vara='window'varobj2={a:'obj2'}obj.foo()()obj.foo.call(obj2)()obj.foo().call(obj2)看着這麼多括號,是不是感覺有幾分頭大。沒事,咱們來一層一層分析:
顯式綁定一開始講的時候,就談過call/apply存在傳參差異,那咱們就來傳一下參數,看看傳完參數的this會是怎樣的美妙。
varobj={a:1,foo:function(b){b=b||this.areturnfunction(c){console.log(this.a+b+c)}}}vara=2varobj2={a:3}obj.foo(a).call(obj2,1)obj.foo.call(obj2)(1)要注意call執行的位置:
obj.foo(a).call(obj2, 1):
obj.foo.call(obj2)(1):
答案
66麻了嗎,兄弟們。進度已經快過半了,休息一會,爭取把this一次性吃透。
上面提了很多call/apply可以改變this指向,但都沒有太多實用性。下面來一起學幾個常用的call與apply使用。
題目5.1:apply求數組最值JavaScript中沒有給數組提供類似max和min函數,只提供了Math.max/min,用於求多個數的最值,所以可以藉助apply方法,直接傳遞數組給Math.max/min
constarr=[1,10,11,33,4,52,17]Math.max.apply(Math,arr)Math.min.apply(Math,arr)題目5.2:類數組轉為數組ES6未發布之前,沒有Array.from方法可以將類數組轉為數組,採用Array.prototype.slice.call(arguments)或[].slice.call(arguments)將類數組轉化為數組。
題目5.3:數組高階函數日常編碼中,我們會經常用到forEach、map等,但這些數組高階方法,它們還有第二個參數thisArg,每一個回調函數都是顯式綁定在thisArg上的。
例如下面這個例子
constobj={a:10}constarr=[1,2,3,4]arr.forEach(function(val,key){console.log(`${key}:${val}---${this.a}`)},obj)答案
0:1---101:2---102:3---103:4---10關於數組高階函數的知識可以參考: JavaScript之手撕高階數組函數
6.new綁定使用new來構建函數,會執行如下四部操作:
關於new更詳細的知識,可以參考:JavaScript之手撕new[7]
通過new來調用構造函數,會生成一個新對象,並且把這個新對象綁定為調用函數的this。
題目6.1:new綁定functionUser(name,age){this.name=name;this.age=age;}varname='Tom';varage=18;varzc=newUser('zc',24);console.log(zc.name)答案
zc題目6.2:屬性加方法functionUser(name,age){this.name=name;this.age=age;this.introduce=function(){console.log(this.name)}this.howOld=function(){returnfunction(){console.log(this.age)}}}varname='Tom';varage=18;varzc=newUser('zc',24)zc.introduce()zc.howOld()()這個題很難不讓人想到如下代碼,都是函數嵌套,具體解法是類似的,可以對比來看一下啊。
constUser={name:'zc';age:18;introduce=function(){console.log(this.name)}howOld=function(){returnfunction(){console.log(this.age)}}}varname='Tom';varage=18;User.introduce()User.howOld()()答案
zc18題目6.3:new界的天王山new界的天王山,每次看懂後,沒過多久就會忘掉,但這次要從根本上弄清楚該題。
接下來一起來品味品味:
functionFoo(){getName=function(){console.log(1);};returnthis;}Foo.getName=function(){console.log(2);};Foo.prototype.getName=function(){console.log(3);};vargetName=function(){console.log(4);};functiongetName(){console.log(5)};Foo.getName();getName();Foo().getName();getName();newFoo.getName();newFoo().getName();newnewFoo().getName();Foo.getName(): 執行Foo上的getName方法,打印2
getName(): 執行GO中的getName方法,打印4
Foo().getName()
//修改全局GO的getName為function(){console.log(1);}getName=function(){console.log(1)}//Foo為默認綁定,this->window//returnwindowreturnthis複製代碼getName(): 執行GO中的getName,打印1
分析後面三個打印結果之前,先補充一些運算符優先級方面的知識(圖源:MDN[8])
從上圖可以看到,部分優先級如下:new(帶參數列表) = 成員訪問 = 函數調用 > new(不帶參數列表)
new Foo.getName()
首先從左往右看:new Foo屬於不帶參數列表的new(優先級19),Foo.getName屬於成員訪問(優先級20),getName()屬於函數調用(優先級20),同樣優先級遵循從左往右執行。
這裡有一個誤區:很多人認為這裡的new是沒做任何操作的的,執行的是函數調用。那麼如果執行的是Foo.getName(),調用返回值為undefined,new undefined會發生報錯,並且我們可以驗證一下該表達式的返回結果。
console.log(newFoo.getName())//2//Foo.getName{}可見在成員訪問之後,執行的是帶參數列表格式的new操作。
new Foo().getName()
new new Foo().getName()
從左往右分析: 第一個new不帶參數列表(優先級19),new Foo()帶參數列表(優先級20),剩下的成員訪問和函數調用優先級都是20
測試結果如下:
foo1=newFoo.getName()foo2=newnewFoo().getName()console.log(foo1.constructor)console.log(foo2.constructor)輸出結果:
23ƒ(){console.log(2);}ƒ(){console.log(3);}通過這一步比較應該能更好的理解上面的執行順序。
答案
2411233兄弟們,革命快要成功了,再努力一把,以後this都小問題啦。
箭頭函數沒有自己的this,它的this指向外層作用域的this,且指向函數定義時的this而非執行時。
上文說到,箭頭函數的this通過作用域鏈查到,intro函數的上層作用域為window。
答案
Mynameistom題目7.2:箭頭函數與普通函數比較name='tom'constobj={name:'zc',intro:function(){return()=>{console.log('Mynameis'+this.name)}},intro2:function(){returnfunction(){console.log('Mynameis'+this.name)}}}obj.intro2()()obj.intro()()答案
obj1obj1windowwindowwindowwindow題目7.4:new碰上箭頭函數functionUser(name,age){this.name=name;this.age=age;this.intro=function(){console.log('Mynameis'+this.name)},this.howOld=()=>{console.log('Myageis'+this.age)}}varname='Tom',age=18;varzc=newUser('zc',24);zc.intro();zc.howOld();箭頭函數由於沒有this,不能通過call\apply\bind來修改this指向,但可以通過修改外層作用域的this來達成間接修改
varname='window'varobj1={name:'obj1',intro:function(){console.log(this.name)return()=>{console.log(this.name)}},intro2:()=>{console.log(this.name)returnfunction(){console.log(this.name)}}}varobj2={name:'obj2'}obj1.intro.call(obj2)()obj1.intro().call(obj2)obj1.intro2.call(obj2)()obj1.intro2().call(obj2)答案
obj2obj2obj1obj1windowwindowwindowobj28.箭頭函數擴展總結DOM中事件的回調函數中this已經封裝指向了調用元素,如果使用構造函數,其this會指向window對象
document.getElementById('btn').addEventListener('click',()=>{console.log(this===window);//true})9.綜合題學完上面的知識,是不是感覺自己已經趨於化境了,現在就一起來華山之巔一決高下吧。
題目9.1: 對象綜合體varname='window'varuser1={name:'user1',foo1:function(){console.log(this.name)},foo2:()=>console.log(this.name),foo3:function(){returnfunction(){console.log(this.name)}},foo4:function(){return()=>{console.log(this.name)}}}varuser2={name:'user2'}user1.foo1()user1.foo1.call(user2)user1.foo2()user1.foo2.call(user2)user1.foo3()()user1.foo3.call(user2)()user1.foo3().call(user2)user1.foo4()()user1.foo4.call(user2)()user1.foo4().call(user2)這個題目並不難,就是把上面很多題做了個整合,如果上面都學會了,此題問題不大。
答案:
varname='window'varuser1={name:'user1',foo1:function(){console.log(this.name)},foo2:()=>console.log(this.name),foo3:function(){returnfunction(){console.log(this.name)}},foo4:function(){return()=>{console.log(this.name)}}}varuser2={name:'user2'}user1.foo1()//user1user1.foo1.call(user2)//user2user1.foo2()//windowuser1.foo2.call(user2)//windowuser1.foo3()()//windowuser1.foo3.call(user2)()//windowuser1.foo3().call(user2)//user2user1.foo4()()//user1user1.foo4.call(user2)()//user2user1.foo4().call(user2)//user1題目9.2:隱式綁定丟失varx=10;varfoo={x:20,bar:function(){varx=30;console.log(this.x)}};foo.bar();(foo.bar)();(foo.bar=foo.bar)();(foo.bar,foo.bar)();突然出現了一個代碼很少的題目,還乍有些不習慣。
上面那說法有可能有幾分難理解,隱式綁定有個定性條件,就是要滿足XXX.fn()格式,如果破壞了這種格式,一般隱式綁定都會丟失。
題目9.3:arguments(推薦看)varlength=10;functionfn(){console.log(this.length);}varobj={length:5,method:function(fn){fn();arguments[0]();}};obj.method(fn,1);這個題要注意一下,有坑。
fn(): 默認綁定,打印10
arguments[0](): 這種執行方式看起來就怪怪的,咱們把它展開來看看:
arguments:{0:fn,1:1,length:2}複製代碼arguments:{fn:fn,1:1,length:2}複製代碼fn.call(null) 或者 fn.call(undefined) 都相當於fn()
obj.fn為立即執行函數: 默認綁定,this指向window
我們來一句一句的分析:
此時的obj可以類似的看成以下代碼(注意存在閉包):
obj={number:3,fn:function(){varnum=this.number;this.number*=2;console.log(num);number*=3;console.log(number);}}複製代碼myFun.call(null): 相當於myFun(),隱式綁定丟失,myFun的this指向window。
依舊一句一句的分析:
obj.fn(): 隱式綁定,fn的this指向obj
繼續一步一步的分析:
console.log(window.number): 打印20
這裡解釋一下,為什麼myFun.call(null)執行時,找不到number變量,是去找立即執行函數AO中的number,而不是找window.number: JavaScript採用的靜態作用域,當定義函數後,作用域鏈就已經定死。(更詳細的解釋文章最開始的推薦中有)
答案
10932720總結this到這裡基本接近尾聲了,鬆了一口氣。這篇文章寫了好久,找資源,修改博文,各種亂七八糟的雜事,導致遲遲寫不出滿意的博文。有可能天生理科男的緣故吧,怎麼寫感覺文章都很生硬,但好在還是順利寫完了。
在文章的最後,感謝一下參考的博客和題目的來源
最後按照阿包慣例,附贈一道面試題:
varnum=10varobj={num:20}obj.fn=(function(num){this.num=num*3num++returnfunction(n){this.num+=nnum++console.log(num)}})(obj.num)varfn=obj.fnfn(5)obj.fn(10)console.log(num,obj.num)最後祝大家都能學好前端,步步登神,成為大佬。
關於本文作者:戰場小包https://juejin.cn/post/7019470820057546766
參考資料https://juejin.cn/post/7019108835197452301
[2]https://blog.csdn.net/qq_32036091/article/details/120608863
[3]https://blog.csdn.net/qq_32036091/article/details/120589645
[4]https://blog.csdn.net/qq_32036091/article/details/120297142
[5]https://blog.csdn.net/qq_32036091/article/details/120518027
[6]https://blog.csdn.net/qq_32036091/article/details/120618424
[8]https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
[9]https://blog.csdn.net/qq_32036091/article/details/120297142
[10]https://juejin.cn/post/6844904083707396109#heading-14
[11]https://juejin.cn/post/6844903805587619854
- EOF -
JS 定時器的 this 指向若干問題總結
看完這篇文章,徹底了解 「原型」 & 「this」
前端面經 - 看這篇就夠了
覺得本文對你有幫助?請分享給更多人
推薦關注「前端開發博客」,提升前端技能
我是漫步,分享技術,不止前端,下期見~
文中如有錯誤,歡迎給我留言,如果這篇文章幫助到了你,歡迎點讚、在看和關注。你的點讚、在看和關注是對我最大的支持!
創作不易,你的每一個點讚、在看、分享都是對我最大的支持!❤️