從這兩套題,從新認識JS的this、做用域、閉包、對象

平常開發中,咱們常常用到this。例如用Jquery綁定事件時,this指向觸發事件的DOM元素;編寫Vue、React組件時,this指向組件自己。對於新手來講,常會用一種意會的感受去判斷this的指向。以致於當遇到複雜的函數調用時,就分不清this的真正指向。javascript

本文將經過兩道題去慢慢分析this的指向問題,並涉及到函數做用域與對象相關的點。最終給你們帶來真正的理論分析,而不是簡簡單單的一句話歸納。java

相信如果對this稍有研究的人,都會搜到這句話:this老是指向調用該函數的對象git

然而箭頭函數並非如此,因而你們就會遇到以下各式說法:github

  1. 箭頭函數的this指向外層函數做用域中的this。
  2. 箭頭函數的this是定義函數時所在上下文中的this。
  3. 箭頭函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。

各式各樣的說法都有,乍看下感受說的差很少。廢話很少說,憑着你以前的理解,來先作一套題吧(非嚴格模式下)。chrome

/**
 * Question 1
 */

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { 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)()

大體意思就是,有兩個對象person1person2,而後花式調用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就是指向誰this

person1.show2()person1.show2.call(person2)的結果用上面的定義解釋,就開始讓人不理解了。

它的執行結果說明this指向的是window。那就不是所謂的定義時所在的對象。

若是說是外層函數做用域中的this,實際上並無外層函數了,外層就是全局環境了,這個說法也不嚴謹。

只有定義函數時所在上下文中的this這句話算能描述如今這個狀況。

person1.show3是一個高階函數,它返回了一個函數,分步走的話,應該是這樣:

var func = 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
 */
var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('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是有本身的函數做用域。可是對於personA來講,它只是一個對象,在直觀感覺上,它跟第一道題中的person1應該是如出一轍的。 JSON.stringify(new Person('person1')) === JSON.stringify(person1)也證實了這一點。

說明構造函數建立對象與直接用字面量的形式去建立對象,它是不一樣的,構造函數建立對象,具體作了什麼事呢?我引用紅寶書中的一段話。

使用 new 操做符調用構造函數,實際上會經歷一下4個步驟:

  1. 建立一個新對象;
  2. 將構造函數的做用域賦給新對象(所以this就指向了這個新對象);
  3. 執行構造函數中的代碼(爲這個新對象添加屬性);
  4. 返回新對象。

因此與字面量建立對象相比,很大一個區別是它多了構造函數的做用域。咱們用chrome查看這二者的做用域鏈就能清晰的知道:

personA的函數的做用域鏈從構造函數產生的閉包開始,而person1的函數做用域僅是global,因而致使this指向的不一樣。咱們發現,要想真正理解this,先得知道到底什麼是做用域,什麼是閉包。

有簡單的說法稱閉包就是可以讀取其餘函數內部變量的函數。然而這是一種閉包現象的描述,而不是它的本質與造成的緣由。

我再次引用紅寶書的文字(便於理解,文字順序稍微調整),來描述這幾個點:

...每一個函數都有本身的執行環境(execution context,也叫執行上下文),每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。

...當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。當代碼在環境中執行時,會建立一個做用域鏈,來保證對執行環境中的全部變量和函數的有序訪問。函數執行以後,棧將環境彈出。

...函數內部定義的函數會將包含函數的活動對象添加到它的做用域鏈中。

具體來講,當咱們 var func = personA.show3() 時,personAshow3函數的活動對象,會一直保存在func的做用域鏈中。只要不銷燬func,那麼show3函數的活動對象就會一直保存在內存中。(chrome的v8引擎對閉包的開銷會有優化)

而構造函數一樣也是閉包的機制,personAshow1方法,是構造函數的內部函數,所以執行了 this.show1 = function () { console.log(this.name) }時,已經把構造函數的活動對象推到了show1函數的做用域鏈中。

咱們再回到this的指向問題。咱們發現,單單是總結規律,或者用一句話歸納,已經難以正確解釋它到底指向誰了,咱們得追本溯源。

紅寶書中說道:

...this引用的是函數執行的環境對象(便於理解,貼上英文原版:It is a reference to the context object that the function is operating on)。
...每一個函數被調用時都會自動獲取兩個特殊變量:this和arguments。內部在搜索這個兩個變量時,只會搜索到其活動對象爲止,永遠不可能直接訪問外部函數中的這兩個變量。

咱們看下MDN中箭頭函數的概念:

一個箭頭函數表達式的語法比一個函數表達式更短,而且不綁定本身的 thisargumentssupernew.target。...箭頭函數會捕獲其所在上下文的 this 值,做爲本身的 this 值。

也就是說,普通狀況下,this指向調用函數時的對象。在全局執行時,則是全局對象。

箭頭函數的this,由於沒有自身的this,因此this只能根據做用域鏈往上層查找,直到找到一個綁定了this的函數做用域(即最靠近箭頭函數的普通函數做用域,或者全局環境),並指向調用該普通函數的對象。

或者從現象來描述的話,即箭頭函數的this指向聲明函數時,最靠近箭頭函數的普通函數的this。但這個this也會由於調用該普通函數時環境的不一樣而發生變化。致使這個現象的緣由是這個普通函數會產生一個閉包,將它的變量對象保存在箭頭函數的做用域中

故而personAshow2方法由於構造函數閉包的關係,指向了構造函數做用域內的this。而

var func = personA.show4.call(personB)

func() // print personB

由於personB調用了personA的show4,使得返回函數func的做用域的this綁定爲personB,進而調用func時,箭頭函數經過做用域找到的第一個明確的this爲personB。進而輸出personB。

講了這麼多,可能仍是有點繞。總之,想充分理解this的前提,必須得先明白js的執行環境、閉包、做用域、構造函數等基礎知識。而後才能得出清晰的結論。

咱們日常在學習過程當中,不免會更傾向於根據經驗去推導結論,或者直接去找一些通俗易懂的描述性語句。然而實際上可能並非最正確的結果。若是想真正掌握它,咱們就應該追本溯源的去研究它的內部機制。

我上述所說也是我本身推導出的結果,即便它不必定正確,但這個推斷思路跟學習過程,我以爲能夠跟你們分享分享。

--閱讀原文 @相學長

--轉載請先通過本人受權。

相關文章
相關標籤/搜索