重新認識JS的this、作用域、閉包、對象
前言
9月4號,今日早讀文章由丁香園@相學長投稿分享。
正文從這開始~
日常開發中,我們經常用到this。例如用Jquery綁定事件時,this指向觸發事件的DOM元素;編寫Vue、React組件時,this指向組件本身。對於新手來說,常會用一種意會的感覺去判斷this的指向。以至於當遇到複雜的函數調用時,就分不清this的真正指向。
本文將通過兩道題去慢慢分析this的指向問題,並涉及到函數作用域與對象相關的點。最終給大家帶來真正的理論分析,而不是簡簡單單的一句話概括。
相信若是對this稍有研究的人,都會搜到這句話:this總是指向調用該函數的對象。
然而箭頭函數並不是如此,於是大家就會遇到如下各式說法:
箭頭函數的this指向外層函數作用域中的this。
箭頭函數的this是定義函數時所在上下文中的this。
箭頭函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
各式各樣的說法都有,乍看下感覺說的差不多。廢話不多說,憑著你之前的理解,來先做一套題吧(非嚴格模式下)。
/**
* Question 1
*/
varname= window
varperson1={
name: person1 ,
show1:function(){
console.log(this.name)
},
show2:()=>console.log(this.name),
show3:function(){
returnfunction(){
console.log(this.name)
}
},
show4:function(){
return()=>console.log(this.name)
}
}
varperson2={name: person2 }
person1.show1()
person1.show1.call(person2)
person1.show2()
person1.show2.call(person2)
person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()
person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
大致意思就是,有兩個對象person1,person2,然後花式調用person1中的四個show方法,預測真正的輸出。
你可以先把自己預測的答案按順序記在本子上,然後再往下拉看正確答案。
正確答案選下:
person1.show1()// person1
person1.show1.call(person2)// person2
person1.show2()// window
person1.show2.call(person2)// window
person1.show3()()// window
person1.show3().call(person2)// person2
person1.show3.call(person2)()// window
person1.show4()()// person1
person1.show4().call(person2)// person1
person1.show4.call(person2)()// person2
對比下你剛剛記下的答案,是否有不一樣呢?讓我們嘗試來最開始那些理論來分析下。
person1.show1()與person1.show1.call(person2)好理解,驗證了誰調用此方法,this就是指向誰。
person1.show2()與person1.show2.call(person2)的結果用上面的定義解釋,就開始讓人不理解了。
它的執行結果說明this指向的是window。那就不是所謂的定義時所在的對象。
如果說是外層函數作用域中的this,實際上並沒有外層函數了,外層就是全局環境了,這個說法也不嚴謹。
只有定義函數時所在上下文中的this這句話算能描述現在這個情況。
person1.show3是一個高階函數,它返回了一個函數,分步走的話,應該是這樣:
varfunc=person3.show()
func()
從而導致最終調用函數的執行環境是window,但並不是window對象調用了它。所以說,this總是指向調用該函數的對象,這句話還得補充一句:在全局函數中,this等於window。
person1.show3().call(person2) 與 person1.show3.call(person2)() 也好理解了。前者是通過person2調用了最終的列印方法。後者是先通過person2調用了person1的高階函數,然後再在全局環境中執行了該列印方法。
person1.show4()(),person1.show4().call(person2)都是列印person1。這好像又印證了那句:箭頭函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。因為即使我用過person2去調用這個箭頭函數,它指向的還是person1。
然而person1.show4.call(person2)()的結果又是person2。this值又發生改變,看來上述那句描述又走不通了。一步步來分析,先通過person2執行了show4方法,此時show4第一層函數的this指向的是person2。所以箭頭函數輸出了person2的name。也就是說,箭頭函數的this指向的是誰調用箭頭函數的外層function,箭頭函數的this就是指向該對象,如果箭頭函數沒有外層函數,則指向window。這樣去理解show2方法,也解釋的通。
這句話就對了么?在我們學習的過程中,我們總是想以總結規律的方法去總結結論,並且希望結論越簡單越容易描述就越好。實際上可能會錯失真理。
下面我們再做另外一個相似的題目,通過構造函數來創建一個對象,並執行相同的4個show 方法。
/**
* Question 2
*/
varname= window
functionPerson(name){
this.name=name;
this.show1=function(){
console.log(this.name)
}
this.show2=()=>console.log(this.name)
this.show3=function(){
returnfunction(){
console.log(this.name)
}
}
this.show4=function(){
return()=>console.log(this.name)
}
}
varpersonA=newPerson( personA )
varpersonB=newPerson( personB )
personA.show1()
personA.show1.call(personB)
personA.show2()
personA.show2.call(personB)
personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()
personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
同樣的,按照之前的理解,再次預計列印結果,把答案記下來,再往下拉看正確答案。
正確答案選下:
personA.show1()// personA
personA.show1.call(personB)// personB
personA.show2()// personA
personA.show2.call(personB)// personA
personA.show3()()// window
personA.show3().call(personB)// personB
personA.show3.call(personB)()// window
personA.show4()()// personA
personA.show4().call(personB)// personA
personA.show4.call(personB)()// personB
我們發現與之前字面量聲明的相比,show2方法的輸出產生了不一樣的結果。為什麼呢?雖然說構造方法Person是有自己的函數作用域。但是對於person1來說,它只是一個對象,在直觀感受上,它跟第一道題中的person1應該是一模一樣的。 JSON.stringify(new Person( person1 )) === JSON.stringify(person1)也證明了這一點。
說明構造函數創建對象與直接用字面量的形式去創建對象,它是不同的,構造函數創建對象,具體做了什麼事呢?我引用紅寶書中的一段話。
使用 new 操作符調用構造函數,實際上會經歷一下4個步驟:
創建一個新對象;
將構造函數的作用域賦給新對象(因此this就指向了這個新對象);
執行構造函數中的代碼(為這個新對象添加屬性);
返回新對象。
所以與字面量創建對象相比,很大一個區別是它多了構造函數的作用域。我們用chrome查看這兩者的作用域鏈就能清晰的知道:
personA的函數的作用域鏈從構造函數產生的閉包開始,而person1的函數作用域僅是global,於是導致this指向的不同。我們發現,要想真正理解this,先得知道到底什麼是作用域,什麼是閉包。
有簡單的說法稱閉包就是能夠讀取其他函數內部變數的函數。然而這是一種閉包現象的描述,而不是它的本質與形成的原因。
我再次引用紅寶書的文字(便於理解,文字順序稍微調整),來描述這幾個點:
...每個函數都有自己的執行環境(execution context,也叫執行上下文),每個執行環境都有一個與之關聯的變數對象,環境中定義的所有變數和函數都保存在這個對象中。
...當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。當代碼在環境中執行時,會創建一個作用域鏈,來保證對執行環境中的所有變數和函數的有序訪問。函數執行之後,棧將環境彈出。
...函數內部定義的函數會將包含函數的活動對象添加到它的作用域鏈中。
具體來說,當我們 var func = personA.show3() 時,personA的show3函數的活動對象,會一直保存在func的作用域鏈中。只要不銷毀func,那麼show3函數的活動對象就會一直保存在內存中。(chrome的v8引擎對閉包的開銷會有優化)
而構造函數同樣也是閉包的機制,personA的show1方法,是構造函數的內部函數,因此執行了 this.show3 = function () { console.log(this.name) }時,已經把構造函數的活動對象推到了show3函數的作用域鏈中。
我們再回到this的指向問題。我們發現,單單是總結規律,或者用一句話概括,已經難以正確解釋它到底指向誰了,我們得追本溯源。
紅寶書中說道:
...this引用的是函數執行的環境對象(便於理解,貼上英文原版:It is a reference to the context object that the function is operating on)。 ...每個函數被調用時都會自動獲取兩個特殊變數:this和arguments。內部在搜索這個兩個變數時,只會搜索到其活動對象為止,永遠不可能直接訪問外部函數中的這兩個變數。
我們看下MDN中箭頭函數的概念:
一個箭頭函數表達式的語法比一個函數表達式更短,並且不綁定自己的 this,arguments,super或 new.target。...箭頭函數會捕獲其所在上下文的 this 值,作為自己的 this 值。
也就是說,普通情況下,this指向調用函數時的對象。在全局執行時,則是全局對象。
箭頭函數的this,因為沒有自身的this,所以this只能根據作用域鏈往上層查找,直到找到一個綁定了this的函數作用域(即最靠近箭頭函數的普通函數作用域,或者全局環境),並指向調用該普通函數的對象。
或者從現象來描述的話,即箭頭函數的this指向聲明函數時,最靠近箭頭函數的普通函數的this。但這個this也會因為調用該普通函數時環境的不同而發生變化。導致這個現象的原因是這個普通函數會產生一個閉包,將它的變數對象保存在箭頭函數的作用域中。
故而personA的show2方法因為構造函數閉包的關係,指向了構造函數作用域內的this。而
varfunc=personA.show4.call(personB)
func()// print personB
因為personB調用了personA的show4,使得返回函數func的作用域的this綁定為personB,進而調用func時,箭頭函數通過作用域找到的第一個明確的this為personB。進而輸出personB。
講了這麼多,可能還是有點繞。總之,想充分理解this的前提,必須得先明白js的執行環境、閉包、作用域、構造函數等基礎知識。然後才能得出清晰的結論。
我們平常在學習過程中,難免會更傾向於根據經驗去推導結論,或者直接去找一些通俗易懂的描述性語句。然而實際上可能並不是最正確的結果。如果想真正掌握它,我們就應該追本溯源的去研究它的內部機制。
我上述所說也是我自己推導出的結果,即使它不一定正確,但這個推斷思路跟學習過程,我覺得可以跟大家分享分享。
關於本文
作者:@相學長
原文:https://github.com/wuomzfx/blog/blob/master/this.md


※React之組件類型
※瀏覽器漏洞挖掘思路
※【第1041期】關鍵請求
※如何進入BATJ前端部?StuQ免費送你價值3000元進階課!
※微交互:APP設計的秘密
TAG:前端早讀課 |