簡單介紹this用法

1.  this 適合你嗎?javascript

  許多文章在介紹 JavaScript 的 this 時都會假設你學過某種面向對象的編程語言,好比 Java、C++ 或 Python 等。但此文面向的讀者是那些不知道 this 是什麼的人。對此本文儘可能不用任何術語來解釋 this 是什麼,以及 this 的用法。java

也許你一直不敢解開 this 的祕密,由於它看起來挺奇怪也挺嚇人的。或許你只在 StackOverflow 說你須要用它的時候(好比在 React 裏實現某個功能)纔會使用。git

在深刻介紹 this 以前,咱們首先須要理解函數式編程和麪向對象編程之間的區別。github

2.  函數式編程 vs 面向對象編程編程

你可能不知道,JavaScript 同時擁有面向對象和函數式的結構,因此你能夠本身選擇用哪一種風格,或者二者都用。數組

很早之前在使用 JavaScript 時就喜歡函數式編程,並且會像躲避瘟疫同樣避開面向對象編程,由於不理解面向對象中的關鍵字,好比 this。瀏覽器

在某種意義上,也許你能夠只專一於一種結構而且徹底忽略另外一種,但這樣你只能是一個 JavaScript 開發者。爲了解釋函數式和麪向對象之間的區別,下面咱們經過一個數組來舉例說明,數組的內容是 Facebook 的好友列表。閉包

假設你要作一個 Web 應用,當用戶使用 Facebook 登陸你的 Web 應用時,須要顯示他們的 Facebook 的好友信息。你須要訪問 Facebook 並得到用戶的好友數據。這些數據多是 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts 等信息。app

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 得到的。如今須要將其轉換成方便你的項目使用的格式。咱們假設你想顯示的好友信息以下:dom

  • 姓名,格式爲`${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 中實現如下功能:

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
    },
    greeting: function() {
      return `Hello, this is ${fullName}'s data!`
    }
  };
}

這樣能正常工做嗎?

不能!

咱們新建的對象可以訪問 initializeFriend 中的一切變量,但不能訪問這個對象自己的屬性或方法。固然你會問,

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

固然能夠。但要是想在 greeting 中加入距離好友生日的天數怎麼辦?咱們最好仍是有辦法在 greeting 中調用 getDaysUntilBirthday。

這時輪到 this 出場了!

 

6. 終於——this 是什麼

this 在不一樣的環境中能夠指代不一樣的東西。默認的全局環境中 this 指代的是全局對象(在瀏覽器中 this 是 window 對象),這沒什麼太大的用途。而在 this 的規則中具備實用性的是這一條:

若是在對象的方法中使用 this,而該方法在該對象的上下文中調用,那麼 this 指代該對象自己。

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

彆着急,咱們一下子就說。

因此,若是咱們想從 greeting 中調用 getDaysUntilBirtyday 咱們只須要寫 this.getDaysUntilBirthday,由於此時的 this 就是對象自己。

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

咱們利用 this 來重寫 initializeFriend:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      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 方法:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    },
    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 不存在。彷佛咱們沒什麼辦法了。

輪到綁定上場了!

 

7. 明確綁定 this

咱們須要明確地將 this 綁定到 bobRossObj 上。咱們能夠經過 bind 實現:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

以前,this 是按照默認的規則設置的。但使用 bind 以後,咱們明確地將 bobRossObj.onFriendClick 中的 this 的值設置爲 bobRossObj 對象自己。

到此爲止,咱們看到了爲何要使用 this,以及爲何要明確地綁定 this。最後咱們來介紹一下,this 其實是箭頭函數。

 

8. 箭頭函數

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

簡而言之,二者的區別在於:

在定義箭頭函數時,無論 this 指向誰,箭頭函數內部的 this 永遠指向同一個東西。

//嗯……這貌似沒什麼用……彷佛跟普通函數的行爲同樣啊?

咱們經過 initializeFriend 舉例說明。假設咱們想添加一個名爲 greeting 的函數:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      function getLastPost() {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

這樣能運行嗎?若是不能,怎樣修改才能運行?

答案是不能。由於 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 沒有定義。

這時能夠用箭頭函數解決這個問題:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const getLastPost = () => {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

上述代碼的規則是:

在定義箭頭函數時,無論 this 指向誰,箭頭函數內部的 this 永遠指向同一個東西。

箭頭函數是在 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

//原文:https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7做者:Austin Tackaberry,Human API 的軟件工程師譯者:彎月,責編:屠敏
相關文章
相關標籤/搜索