close
大家好,我是漫步,今天來分享this相關問題。喜歡記得關注我並設為星標,及時收到更新。

前言

當一個函數調用時,會創建一個執行上下文,這個上下文包括函數調用的一些信息(調用棧,傳入參數,調用方式),this就指向這個執行上下文。

this不是靜態的,也並不是在編寫的時候綁定的,而是在運行時綁定的。它的綁定和函數聲明的位置沒有關係,只取決於函數調用的方式。

本篇文章有點長,涉及到很多道面試題,有難有簡單,如果能耐心的通讀一編,我相信以後this都不成問題。

學習this之前,建議先學習以下知識:

JavaScript之預編譯[1]
JavaScript之手撕new[2]
JavaScript之手撕call/apply[3]
JavaScript之靜態作用域與動態作用域[4]
JavaScript之手撕數組高階函數[5]

在文章的最開始,陳列一下本篇文章涉及的內容,保證讓大家不虛此行。

默認綁定
隱式綁定
隱式綁定丟失
顯式綁定
顯式綁定應用
new綁定
箭頭函數綁定
綜合題
總結
this指向哪裡

在JavaScript中,要想完全理解this,首先要理解this的綁定規則,this的綁定規則一共有5種:

默認綁定
隱式綁定
顯式(硬)綁定
new綁定
ES6新增箭頭函數綁定

下面來一一介紹以下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();
foo(): 默認綁定,打印1
obj.foo(): 隱式綁定,打印2

答案

12

obj是通過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)

用函數預編譯的知識來解答這個問題:函數預編譯四部曲前兩步分別是:

找形參和變量聲明,值賦予undefined
將形參與實參相統一,也就是將實參的值賦予形參。

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)
console.log(this): obj2.doFoo符合xxx.fn格式,doFoo的為隱式綁定,this為obj2,打印{a: 3, doFoo: ƒ}
fn(): 沒有於obj2產生聯繫,默認綁定,打印2

答案

{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是異步調用的,只有當滿足條件並且同步代碼執行完畢後,才會執行它的回調函數。

Tom.introduce()執行: console位於setTimeout的回調函數中,回調函數的this指向window
Mary.introduce直接作為setTimeout的函數參數(類似題目題目3.3),會發生隱式綁定丟失,this為默認綁定
Lisa.introduce執行雖然位於setTimeout的回調函數中,但保持xxx.fn模式,this為隱式綁定。

答案

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指向,但使用起來略有差別:

call()和apply()函數會立即執行
bind()函數會返回新函數,不會立即執行函數
call()和apply()的區別在於call接受若干個參數,apply接受數組。
題目4.1:比較三種調用方式functionfoo(){console.log(this.a)}varobj={a:1}vara=2foo()foo.call(obj)foo.apply(obj)foo.bind(obj)
foo(): 默認綁定。
foo.call(obj): 顯示綁定,foo的this指向obj
foo.apply(obj): 顯式綁定
foo.bind(obj): 顯式綁定,但不會立即執行函數,沒有返回值

答案

211題目4.2:隱式綁定丟失

題目3.4發生隱式綁定的丟失,如下代碼:我們可不可以通過顯式綁定來修正這個問題。

functionfoo(){console.log(this.a)}functiondoFoo(fn){console.log(this)fn()}varobj={a:1,foo}vara=2doFoo(obj.foo)
首先先修正doFoo()函數的this指向。
doFoo.call(obj,obj.foo)
然後修正fn的this。
functionfoo(){console.log(this.a)}functiondoFoo(fn){console.log(this)fn.call(this)}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)
foo(): 默認綁定
foo.call(obj): 顯式綁定
foo().call(obj): 對foo()執行的返回值執行call,foo返回值為undefined,執行call()會報錯

答案

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(): 默認綁定
foo.call(obj): 顯式綁定
foo().call(obj): foo()執行,打印2,返回匿名函數通過call將this指向obj,打印1。

這裡千萬注意:最後一個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。

foo(): 默認綁定,打印2
foo.bind(obj): 返回新函數,不會執行函數,無輸出
foo().bind(obj): 第一層foo(),默認綁定,打印2,後bind將foo()返回的匿名函數this指向obj,不執行

答案

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)

看着這麼多括號,是不是感覺有幾分頭大。沒事,咱們來一層一層分析:

obj.foo()(): 第一層obj.foo()執行為隱式綁定,打印出foo:obj;第二層匿名函數為默認綁定,打印inner:window
obj.foo.call(obj2)(): 類似題目4.7,第一層obj.foo.call(obj2)使用call將obj.foo的this指向obj2,打印foo: obj2;第二層匿名函數默認綁定,打印inner:window
obj.foo().call(obj2): 類似題目4.5,第一層隱式綁定,打印:foo: obj,第二層匿名函數使用call將this指向obj2,打印inner: obj2
題目4.9:帶參數的call

顯式綁定一開始講的時候,就談過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(a): foo的AO中b值為傳入的a(形參與實參相統一),值為2,返回匿名函數fn
匿名函數fn.call(obj2, 1): fn的this指向為obj2,c值為1
this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6

obj.foo.call(obj2)(1):

obj.foo.call(obj2): obj.foo的this指向obj2,未傳入參數,b = this.a = obj2.a = 3;返回匿名函數fn
匿名函數fn(1): c = 1,默認綁定,this指向window
this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6

答案

66

麻了嗎,兄弟們。進度已經快過半了,休息一會,爭取把this一次性吃透。

5.顯式綁定擴展

上面提了很多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來構建函數,會執行如下四部操作:

創建一個空的簡單JavaScript對象(即{});
為步驟1新創建的對象添加屬性__proto__,將該屬性鏈接至構造函數的原型對象 ;
將步驟1新創建的對象作為this的上下文 ;
如果該函數沒有返回對象,則返回this。

關於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()()
zc.introduce(): zc是new創建的實例,this指向zc,打印zc
zc.howOld()(): zc.howOld()返回一個匿名函數,匿名函數為默認綁定,因此打印18(阿包永遠18)

答案

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();
預編譯
GO={Foo:fn(Foo),getName:functiongetName(){console.log(5)};}
分析後續執行

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複製代碼
Foo().getName(): 執行window.getName(),打印1
Foo()執行

getName(): 執行GO中的getName,打印1

分析後面三個打印結果之前,先補充一些運算符優先級方面的知識(圖源:MDN[8])

從上圖可以看到,部分優先級如下:new(帶參數列表) = 成員訪問 = 函數調用 > new(不帶參數列表)

new Foo.getName()

首先從左往右看:new Foo屬於不帶參數列表的new(優先級19),Foo.getName屬於成員訪問(優先級20),getName()屬於函數調用(優先級20),同樣優先級遵循從左往右執行。

Foo.getName執行,獲取到Foo上的getName屬性
此時原表達式變為new (Foo.getName)(),new (Foo.getName)()為帶參數列表(優先級20),(Foo.getName)()屬於函數調用(優先級20),從左往右執行
new (Foo.getName)()執行,打印2,並返回一個以Foo.getName()為構造函數的實例

這裡有一個誤區:很多人認為這裡的new是沒做任何操作的的,執行的是函數調用。那麼如果執行的是Foo.getName(),調用返回值為undefined,new undefined會發生報錯,並且我們可以驗證一下該表達式的返回結果。

console.log(newFoo.getName())//2//Foo.getName{}

可見在成員訪問之後,執行的是帶參數列表格式的new操作。

new Foo().getName()

同步驟4一樣分析,先執行new Foo(),返回一個以Foo為構造函數的實例
Foo的實例對象上沒有getName方法,沿原型鏈查找到Foo.prototype.getName方法,打印3

new new Foo().getName()

從左往右分析: 第一個new不帶參數列表(優先級19),new Foo()帶參數列表(優先級20),剩下的成員訪問和函數調用優先級都是20

new Foo()執行,返回一個以Foo為構造函數的實例
在執行成員訪問,Foo實例對象在Foo.prototype查找到getName屬性
執行new (new Foo().getName)(),返回一個以 Foo.prototype.getName()為構造函數的實例,打印3
new Foo.getName() 與 new new Foo().getName()區別:
new Foo.getName()的構造函數是Foo.getName
new new Foo().getName()的構造函數為Foo.prototype.getName

測試結果如下:

foo1=newFoo.getName()foo2=newnewFoo().getName()console.log(foo1.constructor)console.log(foo2.constructor)

輸出結果:

23ƒ(){console.log(2);}ƒ(){console.log(3);}

通過這一步比較應該能更好的理解上面的執行順序。

答案

2411233

兄弟們,革命快要成功了,再努力一把,以後this都小問題啦。

7.箭頭函數

箭頭函數沒有自己的this,它的this指向外層作用域的this,且指向函數定義時的this而非執行時。

this指向外層作用域的this: 箭頭函數沒有this綁定,但它可以通過作用域鏈查到外層作用域的this
指向函數定義時的this而非執行時: JavaScript是靜態作用域,就是函數定義之後,作用域就定死了,跟它執行時的地方無關。更詳細的介紹見JavaScript之靜態作用域與動態作用域[9]。
題目7.1:對象方法使用箭頭函數name='tom'constobj={name:'zc',intro:()=>{console.log('Mynameis'+this.name)}}obj.intro()

上文說到,箭頭函數的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()()
obj.intro2()(): 不做贅述,打印My name is tom
obj.intro()(): obj.intro()返回箭頭函數,箭頭函數的this取決於它的外層作用域,因此箭頭函數的this指向obj,打印My name is zc
題目7.3:箭頭函數與普通函數的嵌套name='window'constobj1={name:'obj1',intro:function(){console.log(this.name)return()=>{console.log(this.name)}}}constobj2={name:'obj2',intro:()=>{console.log(this.name)returnfunction(){console.log(this.name)}}}constobj3={name:'obj3',intro:()=>{console.log(this.name)return()=>{console.log(this.name)}}}obj1.intro()()obj2.intro()()obj3.intro()()
obj1.intro()(): 類似題目7.2,打印obj1,obj1
obj2.intro()(): obj2.intro()為箭頭函數,this為外層作用域this,指向window。返回匿名函數為默認綁定。打印window,window
obj3.intro()(): obj3.intro()與obj2.intro()相同,返回值為箭頭函數,外層作用域intro的this指向window,打印window,window

答案

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();
zc是new User實例,因此構造函數User的this指向zc
zc.intro(): 打印My name is zc
zc.howOld(): howOld為箭頭函數,箭頭函數this由外層作用域決定,且指向函數定義時的this,外層作用域為User,this指向zc,打印My age is 24
題目7.5:call碰上箭頭函數

箭頭函數由於沒有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)
obj1.intro.call(obj2)(): 第一層函數為普通函數,通過call修改this為obj2,打印obj2。第二層函數為箭頭函數,它的this與外層this相同,同樣打印obj2。
obj1.intro().call(obj2): 第一層函數打印obj1,第二次函數為箭頭函數,call無效,它的this與外層this相同,打印obj1
obj1.intro2.call(obj2)(): 第一層為箭頭函數,call無效,外層作用域為window,打印window;第二次為普通匿名函數,默認綁定,打印window
obj1.intro2().call(obj2): 與上同,打印window;第二層為匿名函數,call修改this為obj2,打印obj2

答案

obj2obj2obj1obj1windowwindowwindowobj28.箭頭函數擴展總結
箭頭函數沒有this,它的this是通過作用域鏈查到外層作用域的this,且指向函數定義時的this而非執行時。
不可以用作構造函數,不能使用new命令,否則會報錯
箭頭函數沒有arguments對象,如果要用,使用rest參數代替
不可以使用yield命令,因此箭頭函數不能用作Generator函數。
不能用call/apply/bind修改this指向,但可以通過修改外層作用域的this來間接修改。
箭頭函數沒有prototype屬性。
避免使用場景
箭頭函數定義對象方法
constzc={name:'zc',intro:()=>{//this->windowconsole.log(this.name)}}zc.intro()//undefined
箭頭函數不能作為構造函數
constUser=(name,age)=>{this.name=name;this.age=age;}//UncaughtTypeError:Userisnotaconstructorzc=newUser('zc',24);
事件的回調函數

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)

這個題目並不難,就是把上面很多題做了個整合,如果上面都學會了,此題問題不大。

user1.foo1()、user1.foo1.call(user2): 隱式綁定與顯式綁定
user1.foo2()、user1.foo2.call(user2): 箭頭函數與call
user1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2): 見題目4.8
user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2): 見題目7.5

答案:

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)();

突然出現了一個代碼很少的題目,還乍有些不習慣。

foo.bar(): 隱式綁定,打印20
(foo.bar)(): 上面提到過運算符優先級的知識,成員訪問與函數調用優先級相同,默認從左到右,因此括號可有可無,隱式綁定,打印20
(foo.bar = foo.bar)():隱式綁定丟失,給foo.bar起別名,雖然名字沒變,但是foo.bar上已經跟foo無關了,默認綁定,打印10
(foo.bar, foo.bar)(): 隱式綁定丟失,起函數別名,將逗號表達式的值(第二個foo.bar)賦值給新變量,之後執行新變量所指向的函數,默認綁定,打印10。

上面那說法有可能有幾分難理解,隱式綁定有個定性條件,就是要滿足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函數this指向arguments,打印2
arguments[0]: 這是訪問對象的屬性0?0不好理解,咱們把它稍微一換,方便一下理解:
arguments是一個類數組,arguments展開,應該是下面這樣:
題目9.4:壓軸題(推薦看)varnumber=5;varobj={number:3,fn:(function(){varnumber;this.number*=2;number=number*2;number=3;returnfunction(){varnum=this.number;this.number*=2;console.log(num);number*=3;console.log(number);}})()}varmyFun=obj.fn;myFun.call(null);obj.fn();console.log(window.number);

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);}}複製代碼
var number: 立即執行函數的AO中添加number屬性,值為undefined
this.number *= 2: window.number = 10
number = number * 2: 立即執行函數AO中number值為undefined,賦值後為NaN
number = 3: AO中number值由NaN修改為3
返回匿名函數,形成閉包

myFun.call(null): 相當於myFun(),隱式綁定丟失,myFun的this指向window。

依舊一句一句的分析:

var num = this.number: this指向window,num = window.num = 10
this.number *= 2: window.number = 20
console.log(num): 打印10
number *= 3: 當前AO中沒有number屬性,沿作用域鏈可在立即執行函數的AO中查到number屬性,修改其值為9
console.log(number): 打印立即執行函數AO中的number,打印9

obj.fn(): 隱式綁定,fn的this指向obj

繼續一步一步的分析:

var num = this.number: this->obj,num = obj.num = 3
this.number *= 2: obj.number *= 2 = 6
console.log(num): 打印num值,打印3
number *= 3: 當前AO中不存在number,繼續修改立即執行函數AO中的number,number *= 3 = 27
console.log(number): 打印27

console.log(window.number): 打印20

這裡解釋一下,為什麼myFun.call(null)執行時,找不到number變量,是去找立即執行函數AO中的number,而不是找window.number: JavaScript採用的靜態作用域,當定義函數後,作用域鏈就已經定死。(更詳細的解釋文章最開始的推薦中有)

答案

10932720總結
默認綁定: 非嚴格模式下this指向全局對象,嚴格模式下this會綁定到undefined
隱式綁定: 滿足XXX.fn()格式,fn的this指向XXX。如果存在鏈式調用,this永遠指向最後調用它的那個對象
隱式綁定丟失:起函數別名,通過別名運行;函數作為參數會造成隱式綁定丟失。
顯示綁定: 通過call/apply/bind修改this指向
new綁定: 通過new來調用構造函數,會生成一個新對象,並且把這個新對象綁定為調用函數的this。
箭頭函數綁定: 箭頭函數沒有this,它的this是通過作用域鏈查到外層作用域的this,且指向函數定義時的this而非執行時
後語

this到這裡基本接近尾聲了,鬆了一口氣。這篇文章寫了好久,找資源,修改博文,各種亂七八糟的雜事,導致遲遲寫不出滿意的博文。有可能天生理科男的緣故吧,怎麼寫感覺文章都很生硬,但好在還是順利寫完了。

在文章的最後,感謝一下參考的博客和題目的來源

霖呆呆大佬[10]
小夕大佬:嗨,你真的懂this嗎?[11]
渡一教育的題源

最後按照阿包慣例,附贈一道面試題:

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

參考資料
[1]

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」

前端面經 - 看這篇就夠了

覺得本文對你有幫助?請分享給更多人

推薦關注「前端開發博客」,提升前端技能


我是漫步,分享技術,不止前端,下期見~

文中如有錯誤,歡迎給我留言,如果這篇文章幫助到了你,歡迎點讚、在看和關注。你的點讚、在看和關注是對我最大的支持!

創作不易,你的每一個點讚、在看、分享都是對我最大的支持!❤️

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()