天天看點

JavaScript 的這個難點,毀掉了多少程式員?

1.  this 适合你嗎?

我看到許多文章在介紹 JavaScript 的 this 時都會假設你學過某種面向對象的程式設計語言,比如 Java、C++ 或 Python 等。但這篇文章面向的讀者是那些不知道 this 是什麼的人。我盡量不用任何術語來解釋 this 是什麼,以及 this 的用法。

也許你一直不敢解開 this 的秘密,因為它看起來挺奇怪也挺吓人的。或許你隻在 StackOverflow 說你需要用它的時候(比如在 React 裡實作某個功能)才會使用。

在深入介紹 this 之前,我們首先需要了解函數式程式設計和面向對象程式設計之間的差別。

2.  函數式程式設計 vs 面向對象程式設計

你可能不知道,JavaScript 同時擁有面向對象和函數式的結構,是以你可以自己選擇用哪種風格,或者兩者都用。

我在很早以前使用 JavaScript 時就喜歡函數式程式設計,而且會像躲避瘟疫一樣避開面向對象程式設計,因為我不了解面向對象中的關鍵字,比如 this。我不知道為什麼要用 this。似乎沒有它我也可以做好所有的工作。

而且我是對的。

在某種意義上 。也許你可以隻專注于一種結構并且完全忽略另一種,但這樣你隻能是一個 JavaScript 開發者。為了解釋函數式和面向對象之間的差別,下面我們通過一個數組來舉例說明,數組的内容是 Facebook 的好友清單。

假設你要做一個 Web 應用,當使用者使用 Facebook 登入你的 Web 應用時,需要顯示他們的 Facebook 的好友資訊。你需要通路 Facebook 并獲得使用者的好友資料。這些資料可能是 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts 等資訊。

const data = [

  {

    firstName: 'Bob',

    lastName: 'Ross',

    username: 'bob.ross',    

    numFriends: 125,

    birthday: '2/23/1985',

    lastTenPosts: ['What a nice day', 'I love Kanye West', ...],

  },

  ...

]

假設上述資料是你通過 Facebook API 獲得的。現在需要将其轉換成友善你的項目使用的格式。我們假設你想顯示的好友資訊如下:

 ●  姓名,格式為`${firstName} ${lastName}`

 ●  三篇随機文章

 ●  距離生日的天數

3. 函數式方式

函數式的方式就是将整個數組或者數組中的某個元素傳遞給某個函數,然後傳回你需要的資訊:

const fullNames = getFullNames(data)

// ['Ross, Bob', 'Smith, Joanna', ...]

首先我們有 Facebook API 傳回的原始資料。為了将其轉換成需要的格式,首先要将資料傳遞給一個函數,函數的輸出是(或者包含)經過修改的資料,這些資料可以在應用中向使用者展示。

我們可以用類似的方法獲得随機三篇文章,并且計算距離好友生日的天數。

函數式的方式是:将原始資料傳遞給一個函數或者多個函數,獲得對你的項目有用的資料格式。

4. 面向對象的方式

對于程式設計初學者和 JavaScript 初學者,面向對象的概念可能有點難以了解。其思想是,我們要将每個好友變成一個對象,這個對象能夠生成你一切開發者需要的東西。

你可以建立一個對象,這個對象對應于某個好友,它有 fullName 屬性,還有兩個函數 getThreeRandomPosts 和 getDaysUntilBirthday。

function initializeFriend(data) {

  return {

    fullName: `${data.firstName} ${data.lastName}`,

    getThreeRandomPosts: function() {

      // get three random posts from data.lastTenPosts

    },

    getDaysUntilBirthday: function() {

      // use data.birthday to get the num days until birthday

    }

  };

}

const objectFriends = data.map(initializeFriend)

objectFriends[0].getThreeRandomPosts() 

// Gets three of Bob Ross's posts

面向對象的方式就是為資料建立對象,每個對象都有自己的狀态,并且包含必要的資訊,能夠生成需要的資料。

5. 這跟 this 有什麼關系?

你也許從來沒想過要寫上面的 initializeFriend 代碼,而且你也許認為,這種代碼可能會很有用。但你也注意到,這并不是真正的面向對象。

其原因就是,上面例子中的 getThreeRandomPosts 或 getdaysUntilBirtyday 能夠正常工作的原因其實是閉包。因為使用了閉包,它們在 initializeFriend 傳回之後依然能通路 data。關于閉包的更多資訊可以看看這篇文章:作用域和閉包(https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch5.md)。

還有一個方法該怎麼處理?我們假設這個方法叫做 greeting。注意方法(與 JavaScript 的對象有關的方法)其實隻是一個屬性,隻不過屬性值是函數而已。我們想在 greeting 中實作以下功能:

    greeting: function() {

      return `Hello, this is ${fullName}'s data!`

這樣能正常工作嗎?

不能!

我們建立的對象能夠通路 initializeFriend 中的一切變量,但不能通路這個對象本身的屬性或方法。當然你會問,

難道不能在 greeting 中直接用 data.firstName 和 data.lastName 嗎?

當然可以。但要是想在 greeting 中加入距離好友生日的天數怎麼辦?我們最好還是有辦法在 greeting 中調用 getDaysUntilBirthday。

這時輪到 this 出場了!

JavaScript 的這個難點,毀掉了多少程式員?

6. 終于——this 是什麼

this 在不同的環境中可以指代不同的東西。預設的全局環境中 this 指代的是全局對象(在浏覽器中 this 是 window 對象),這沒什麼太大的用途。而在 this 的規則中具有實用性的是這一條:

如果在對象的方法中使用 this,而該方法在該對象的上下文中調用,那麼 this 指代該對象本身。

你會說“在該對象的上下文中調用”……是啥意思?

别着急,我們一會兒就說。

是以,如果我們想從 greeting 中調用 getDaysUntilBirtyday 我們隻需要寫 this.getDaysUntilBirthday,因為此時的 this 就是對象本身。

附注:不要在全局作用域的普通函數或另一個函數的作用域中使用 this!this 是個面向對象的東西,它隻在對象的上下文(或類的上下文)中有意義。

我們利用 this 來重寫 initializeFriend:

    lastTenPosts: data.lastTenPosts,

    birthday: data.birthday,    

      // get three random posts from this.lastTenPosts

      // use this.birthday to get the num days until birthday

      const numDays = this.getDaysUntilBirthday()      

      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`

現在,在 initializeFriend 執行結束後,該對象需要的一切都位于對象本身的作用域之内了。我們的方法不需要再依賴于閉包,它們隻會用到對象本身包含的資訊。

好吧,這是 this 的用法之一,但你說過 this 在不同的上下文中有不同的含義。那是什麼意思?為什麼不一定會指向對象自己?

有時候,你需要将 this 指向某個特定的東西。一種情況就是事件處理函數。比如我們希望在使用者點選好友時打開好友的 Facebook 首頁。我們會給對象添加下面的 onClick 方法:

    birthday: data.birthday,

    username: data.username,    

    onFriendClick: function() {

      window.open(`https://facebook.com/${this.username}`)

注意我們在對象中添加了 username 屬性,這樣 onFriendClick 就能通路它,進而在新視窗中打開該好友的 Facebook 首頁。現在隻需要編寫 HTML:

<button id="Bob_Ross">

  <!-- A bunch of info associated with Bob Ross -->

</button> 

還有 JavaScript:

const bobRossObj = initializeFriend(data[0])

const bobRossDOMEl = document.getElementById('Bob_Ross')

bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

在上述代碼中,我們給 Bob Ross 建立了一個對象。然後我們拿到了 Bob Ross 對應的 DOM 元素。然後執行 onFriendClick 方法來打開 Bob 的 Facebook 首頁。似乎沒問題,對吧?

有問題!

哪裡出錯了?

注意我們調用 onclick 處理程式的代碼是 bobRossObj.onFriendClick。看到問題了嗎?要是寫成這樣的話能看出來嗎?

bobRossDOMEl.addEventListener("onclick", function() {

  window.open(`https://facebook.com/${this.username}`)

})

現在看到問題了嗎?如果把事件處理程式寫成 bobRossObj.onFriendClick,實際上是把 bobRossObj.onFriendClick 上儲存的函數拿出來,然後作為參數傳遞。它不再“依附”在 bobRossObj 上,也就是說,this 不再指向 bobRossObj。它實際指向全局對象,也就是說 this.username 不存在。似乎我們沒什麼辦法了。

輪到綁定上場了!

JavaScript 的這個難點,毀掉了多少程式員?

7. 明确綁定 this

我們需要明确地将 this 綁定到 bobRossObj 上。我們可以通過 bind 實作:

bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)

之前,this 是按照預設的規則設定的。但使用 bind 之後,我們明确地将 bobRossObj.onFriendClick 中的 this 的值設定為 bobRossObj 對象本身。

到此為止,我們看到了為什麼要使用 this,以及為什麼要明确地綁定 this。最後我們來介紹一下,this 實際上是箭頭函數。

8. 箭頭函數

你也許注意到了箭頭函數最近很流行。人們喜歡箭頭函數,因為很簡潔、很優雅。而且你還知道箭頭函數和普通函數有點差別,盡管不太清楚具體差別是什麼。

簡而言之,兩者的差別在于:

在定義箭頭函數時,不管 this 指向誰,箭頭函數内部的 this 永遠指向同一個東西。

嗯……這貌似沒什麼用……似乎跟普通函數的行為一樣啊?

我們通過 initializeFriend 舉例說明。假設我們想添加一個名為 greeting 的函數:

      function getLastPost() {

        return this.lastTenPosts[0]

      }

      const lastPost = getLastPost()           

      return `Hello, this is ${this.fullName}'s data!

             ${this.fullName}'s last post was ${lastPost}.`

這樣能運作嗎?如果不能,怎樣修改才能運作?

答案是不能。因為 getLastPost 沒有在對象的上下文中調用,是以getLastPost 中的 this 按照預設規則指向了全局對象。

你說沒有“在對象的上下文中調用”……難道它不是從 initializeFriend 傳回的内部調用的嗎?如果這還不叫“在對象的上下文中調用”,那我就不知道什麼才算了。

我知道“在對象的上下文中調用”這個術語很模糊。也許,判斷函數是否“在對象的上下文中調用”的好方法就是檢查一遍函數的調用過程,看看是否有個對象“依附”到了函數上。

我們來檢查下執行 bobRossObj.onFriendClick() 時的情況。“給我對象 bobRossObj,找到其中的 onFriendClick 然後調用該屬性對應的函數”。

我們同樣檢查下執行 getLastPost() 時的情況。“給我名為 getLastPost 的函數然後執行。”看到了嗎?我們根本沒有提到對象。

好了,這裡有個難題來測試你的了解程度。假設有個函數名為 functionCaller,它的功能就是調用一個函數:

functionCaller(fn) {

  fn()

如果調用 functionCaller(bobRossObj.onFriendClick) 會怎樣?你會認為 onFriendClick 是“在對象的上下文中調用”的嗎?this.username有定義嗎?

我們來檢查一遍:“給我 bobRosObj 對象然後查找其屬性 onFriendClick。取出其中的值(這個值碰巧是個函數),然後将它傳遞給 functionCaller,取名為 fn。然後,執行名為 fn 的函數。”注意該函數在調用之前已經從 bobRossObj 對象上“脫離”了,是以并不是“在對象的上下文中調用”的,是以 this.username 沒有定義。

這時可以用箭頭函數解決這個問題:

      const getLastPost = () => {

上述代碼的規則是:

箭頭函數是在 greeting 中定義的。我們知道,在 greeting 内部的 this 指向對象本身。是以,箭頭函數内部的 this 也指向對象本身,這正是我們需要的結果。

9. 結論

this 有時很不好了解,但它對于開發 JavaScript 應用非常有用。本文當然沒能介紹 this 的所有方面。一些沒有涉及到的話題包括:

 ●  call 和 apply; 

 ●  使用 new 時 this 會怎樣;

 ●  在 ES6 的 class 中 this 會怎樣。

我建議你首先問問自己在這些情況下的 this,然後在浏覽器中執行代碼來檢驗你的結果。

想學習更多關 于this 的内容,可參考《你不知道的 JS:this 和對象原型》:

 ●  https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes

如果你想測試自己的知識,可參考《你不知道的JS練習:this和對象原型》:

 ●  https://ydkjs-exercises.com/this-object-prototypes

原文釋出時間為:2018-11-19

本文來自雲栖社群合作夥伴“

前端大學

”,了解相關資訊可以關注“

”。