1. 程式人生 > >深度解析JavaScript的this關鍵字

深度解析JavaScript的this關鍵字

摘要: 神奇的this。

Fundebug經授權轉載,版權歸原作者所有。

這篇文章通過簡單的術語和一個真實的例子解釋了 this 是什麼以及為什麼說它很有用。

你的 this

我發現,很多教程在解釋 JavaScript 的 this 時,通常會假設你擁有 Java、C++ 或 Python 等面向物件程式語言的背景。這篇文章主要面向那些對 this 沒有先入之見的人。我將嘗試解釋什麼是 this 以及為什麼它很有用。

或許你遲遲不肯深入探究 this,因為它看起來很奇怪,讓你心生畏懼。你之所以使用它,有可能僅僅是因為 StackOverflow 說你需要在 React 用它來完成一些事情。

在我們深入瞭解它的真正含義以及為什麼要使用它之前,我們首先需要了解函數語言程式設計和麵向物件程式設計之間的區別。

函數語言程式設計與面向物件程式設計

你可能知道也可能不知道,JavaScript 具有函式和麵向物件的構造,你可以選擇關注其中一個或兩者兼而有之。

在我的 JavaScript 之旅的早期,我一方面擁抱函數語言程式設計,一方面像避免瘟疫一樣排斥面向物件程式設計。我對面向物件關鍵字 this 不甚瞭解。其中的一個原因是我不明白它存在的必要性。在我看來,完全可以不依賴 this 就可以完成所有的事情。

在某種程度上,我的看法是對的。

你可能只關注其中一種正規化而從來不去了解另外一種,作為一名 JavaScript 開發者,你的侷限性就體現在這裡。為了說明函數語言程式設計和麵向物件程式設計之間的差別,我將使用一組 Facebook 好友資料作為示例。

假設你正在構建一個使用者登入 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}
  • 三篇隨機的帖子;
  • 從他們生日起到現在的天數。

函式式方法

如果使用函式式方法,就是將整個陣列或陣列的每個元素傳給一個返回所需操作資料的函式:

const fullNames = getFullNames(data)
// ['Ross, Bob', 'Smith, Joanna', ...]

你從原始資料開始(來自 Facebook API),為了將它們轉換為對你有用的資料,你將資料傳給一個函式,這個函式將輸出你可以在應用程式中顯示給使用者的資料。

你也可以通過類似的方式獲取三個隨機帖子並計算朋友生日至今的天數。

函式式方法就是指接受原始資料,將資料傳給一個或多個函式,並輸出對你有用的資料。

面向物件方法

對於那些剛接觸程式設計和學習 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

面向物件方法是為你的資料建立物件,這些物件包含了狀態和用於生成對你和你的專案有用的資料的資訊。

這與 this 有什麼關係?

你可能沒有想過會寫出類似 initializeFriend 這樣的東西,你可能會認為它很有用。你可能還會注意到,它其實並非真正的面向物件。

getThreeRandomPosts 或 getDaysUntilBirthday 方法之所以有用,主要是因為閉包。因為使用了閉包,所以在 initializeFriend 返回之後,它們仍然可以訪問 data。

假設你寫了另一個方法,叫 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 的變數,但物件本身的屬性或方法不行。當然,你可能會問:

難道你不能用 data.firstName 和 data.lastName 來返回 greeting 嗎?

當然可以。但如果我們還想在 greeting 中包含朋友生日至今的天數,該怎麼辦?我們必須以某種方式從 greeting 中呼叫 getDaysUntilBirthday 方法。

是時候讓 this 上場了!

那麼,this 是什麼

在不同的情況下,this 代表的東西也不一樣。預設情況下,this 指向全域性物件(在瀏覽器中,就是 window 物件)。但光知道這點對我們並沒有太大幫助,對我來說有用的是 this 的這條規則:

如果 this 被用在一個物件的方法中,並且這個方法在物件的上下文中呼叫,那麼 this 就指向這個物件本身。

你會問:“在物件的上下文中呼叫……這又是什麼意思”?

別擔心,稍後我們會解釋這個。

因此,如果我們想在 greeting 中呼叫 getDaysUntilBirthday,可以直接呼叫 this.getDaysUntilBirthday,因為在這種情況下,this 指向物件本身。

注意:不要在全域性作用域或在另一個函式作用域內的常規 ole 函式中使用 this!this 是一個面向物件的構造。因此,它只在物件(或類)的上下文中有意義!

讓我們重構 initializeFriend,讓它使用 this:

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!`
    }
  };
}

現在,在執行完 intializeFriend 後,這個物件的所有東西都限定在物件本身。我們的方法不再依賴於閉包,它們將使用物件本身包含的資訊。

這是 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}`)
})

現在你看到問題所在了嗎?當我們將 onclick 處理程式指定為 bobRossObj.onFriendClick 時,我們實際上是將 bobRossObj.onFriendClick 的函式作為引數傳給了處理程式。它不再“屬於”bobRossObj,也就是說 this 不再指向 bobRossObj。這個時候 this 實際上指向的是全域性物件,所以 this.username 是 undefined 的。

是時候讓 bind 上場了!

顯式繫結 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 處於什麼位置,它指的都是相同的東西。

讓我們用 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 會回退到預設規則,即指向全域性物件。

“在物件的上下文中呼叫”可能是一個比較含糊的概念。要確定一個函式是否是在“物件的上下文中”被呼叫,最好的辦法是看一下函式是如何被呼叫的,以及是否有物件“附加”在函式上。

讓我們來看看執行 bobRossObj.onFriendClick() 時會發生什麼:“找到 bobRossObj 物件的 onFriendClick 屬性,呼叫分配給這個屬性的函式”。

再讓我們來看看執行 getLastPost() 時會發生什麼:”呼叫一個叫作 getLastPost 的函式”。有沒有注意到,這裡並沒有提及任何物件?

現在來測試一下。假設有一個叫作 functionCaller 的函式,它所做的事情就是呼叫其他函式:

functionCaller(fn) {
  fn()
}

如果我們這樣做會怎樣:functionCaller(bobRossObj.onFriendClick)?可不可以說 onFriendClick 是“在物件的上下文中”被呼叫的?this.username 的定義存在嗎?

讓我們來看一下:“找到 bobRossObj 物件的 onFriendClick 屬性。找到這個屬性的值(恰好是一個函式),將它傳給 functionCaller,並命名為 fn。現在,執行名為 fn 的函式”。請注意,函式在被呼叫之前已經從 bobRossObj 物件中“分離”,因此不是“在物件 bobRossObj 的上下文中”呼叫,所以 this.username 是 undefined 的。

讓箭頭函式來救場:

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}`)
    }
  };
}

箭頭函式是在 greeting 中宣告的。我們知道,當我們在 greeting 中使用 this 時,它指向物件本身。因此,箭頭函式中的 this 指向的物件就是我們想要的。

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了7億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!