[譯] this(他喵的)究竟是什麼 — 理解 JavaScript 中的 this、call、apply 和 bind

JavaScript 中最容易被誤解的一點就是 this 關鍵字。在這篇文章中,你將會了解四種規則,弄清楚 this 關鍵字指的是什麼。隱式綁定、顯式綁定、new 綁定和 window 綁定。在介紹這些技術時,你還將學習一些 JavaScript 其餘使人困惑的部分,例如 .call.apply.bindnew 關鍵字。javascript

視頻

正文

在深刻了解 JavaScript 中的 this 關鍵字以前,有必要先退一步,看一下爲何 this 關鍵字很重要。this 容許複用函數時使用不一樣的上下文。換句話說,「this」 關鍵字容許在調用函數或方法時決定哪一個對象應該是焦點。 以後討論的全部東西都是基於這個理念。咱們但願可以在不一樣的上下文或在不一樣的對象中複用函數或方法。前端

咱們要關注的第一件事是如何判斷 this 關鍵字的引用。當你試圖回答這個問題時,你須要問本身的第一個也是最重要的問題是「這個函數在哪裏被調用?」。判斷 this 引用什麼的 惟一 方法就是看使用 this 關鍵字的這個方法在哪裏被調用的。java

用一個你已經十分熟悉的例子來展現這一點,好比咱們有一個 greet 方法,它接受一個名字參數並顯示有歡迎消息的警告框。android

function greet (name) {
  alert(`Hello, my name is ${name}`)
}
複製代碼

若是我問你 greet 會具體警告什麼內容,你會怎樣回答?只給出函數定義是不可能知道答案的。爲了知道 name 是什麼,你必須看看 greet 函數的調用過程。ios

greet('Tyler')
複製代碼

判斷 this 關鍵字引用什麼也是一樣的道理,你甚至能夠把 this 當成一個普通的函數參數對待 — 它會隨着函數調用方式的變化而變化。git

如今咱們知道爲了判斷 this 的引用必須先看函數的定義,在實際地查看函數定義時,咱們設立了四條規則來查找引用,它們是github

  1. 隱式綁定
  2. 顯式綁定
  3. new 綁定
  4. window 綁定

隱式綁定

請記住,這裏的目標是查看使用 this 關鍵字的函數定義,並判斷 this 的指向。執行綁定的第一個也是最多見的規則稱爲 隱式綁定。80% 的狀況下它會告訴你 this 關鍵字引用的是什麼。後端

假如咱們有一個這樣的對象數組

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  }
}
複製代碼

如今,若是你要調用 user 對象上的 greet 方法,你會用到點號。bash

user.greet()
複製代碼

這就把咱們帶到隱式綁定規則的主要關鍵點。爲了判斷 this 關鍵字的引用,函數被調用時先看一看點號左側。若是有「點」就查看點左側的對象,這個對象就是 this 的引用。

在上面的例子中,user 在「點號左側」意味着 this 引用了 user 對象。因此就好像greet 方法的內部 JavaScript 解釋器把 this 變成了 user

greet() {
  // alert(`Hello, my name is ${this.name}`)
  alert(`Hello, my name is ${user.name}`) // Tyler
}
複製代碼

咱們來看一個相似但稍微高級點的例子。如今,咱們的對象不只要擁有 nameagegreet 屬性,還要被添加一個 mother 屬性,而且此屬性也擁有 namegreet 屬性。

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: 'Stacey',
    greet() {
      alert(`Hello, my name is ${this.name}`)
    }
  }
}
複製代碼

如今問題變成下面的每一個函數調用會警告什麼?

user.greet()
user.mother.greet()
複製代碼

每當判斷 this 的引用時,咱們都須要查看調用過程,並確認「點的左側」是什麼。第一個調用,user 在點左側意味着 this 將引用 user。第二次調用中,mother 在點的左側意味着 this 引用 mother

user.greet() // Tyler
user.mother.greet() // Stacey
複製代碼

如前所述,大約有 80% 的狀況下在「點的左側」都會有一個對象。這就是爲何在判斷 this 指向時「查看點的左側」是你要作的第一件事。可是,若是沒有點呢?這就爲咱們引出了下一條規則 —

顯式綁定

若是 greet 函數不是 user 對象的函數,只是一個獨立的函數。

function greet () {
  alert(`Hello, my name is ${this.name}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}
複製代碼

咱們知道爲了判斷 this 的引用咱們首先必須查看這個函數的調用位置。如今就引出了一個問題,咱們怎樣能讓 greet 方法調用的時候將 this 指向 user 對象?。咱們不能再像以前那樣簡單的使用 user.greet(),由於 user 並無 greet 方法。在 JavaScript 中,每一個函數都包含了一個能讓你剛好解決這個問題的方法,這個方法的名字叫作 call

「call」 是每一個函數都有的一個方法,它容許你在調用函數時爲函數指定上下文。

考慮到這一點,用下面的代碼能夠在調用 greet 時用 user 作上下文。

greet.call(user)
複製代碼

再強調一遍,call 是每一個函數都有的一個屬性,而且傳遞給它的第一個參數會做爲函數被調用時的上下文。換句話說,this 將會指向傳遞給 call 的第一個參數。

這就是第 2 條規則的基礎(顯示綁定),由於咱們明確地(使用 .call)指定了 this 的引用。

如今讓咱們對 greet 方法作一點小小的改動。假如咱們想傳一些參數呢?不只提示他們的名字,還要提示他們知道的語言。就像下面這樣

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}
複製代碼

如今爲了將這些參數傳遞給使用 .call 調用的函數,你須要在指定上下文(第一個參數)後一個一個地傳入。

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

greet.call(user, languages[0], languages[1], languages[2])
複製代碼

方法奏效,它顯示瞭如何將參數傳遞給使用 .call 調用的函數。不過你可能注意到,必須一個一個傳遞 languages 數組的元素,這樣有些惱人。若是咱們能夠把整個數組做爲第二個參數並讓 JavaScript 爲咱們自動展開就行了。有個好消息,這就是 .apply 乾的事情。.apply.call 本質相同,但不是一個一個傳遞參數,你能夠用數組傳參並且 .apply 會在函數中爲你自動展開。

那麼如今用 .apply,咱們的代碼能夠改成下面這個,其餘一切都保持不變。

const languages = ['JavaScript', 'Ruby', 'Python']

// greet.call(user, languages[0], languages[1], languages[2])
greet.apply(user, languages)
複製代碼

到目前爲止,咱們學習了關於 .call.apply 的「顯式綁定」規則,用此規則調用的方法可讓你指定 this 在方法內的指向。關於這個規則的最後一個部分是 .bind.bind.call 徹底相同,除了不會馬上調用函數,而是返回一個能之後調用的新函數。所以,若是咱們看看以前所寫的代碼,換用 .bind,它看起來就像這樣

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // alerts "Hello, my name is Tyler and I know JavaScript, Ruby, and Python"
複製代碼

new 綁定

第三條判斷 this 引用的規則是 new 綁定。若你不熟悉 JavaScript 中的 new 關鍵字,其實每當用 new 調用函數時,JavaScript 解釋器都會在底層建立一個全新的對象並把這個對象當作 this。若是用 new 調用一個函數,this 會天然地引用解釋器建立的新對象。

function User (name, age) {
  /*
    JavaScript 會在底層建立一個新對象 `this`,它會代理不在 User 原型鏈上的屬性。
    若是一個函數用 new 關鍵字調用,this 就會指向解釋器建立的新對象。
  */

  this.name = name
  this.age = age
}

const me = new User('Tyler', 27)
複製代碼

window 綁定

假如咱們有下面這段代碼

function sayAge () {
  console.log(`My age is ${this.age}`)
}

const user = {
  name: 'Tyler',
  age: 27
}
複製代碼

如前所述,若是你想用 user 作上下文調用 sayAge,你可使用 .call.apply.bind。但若是咱們沒有用這些方法,而是直接和平時同樣直接調用 sayAge 會發生什麼呢?

sayAge() // My age is undefined
複製代碼

不出意外,你會獲得 My name is undefined,由於 this.age 是 undefined。事情開始變得神奇了。實際上這是由於點的左側沒有任何東西,咱們也沒有用 .call.apply.bind 或者 new 關鍵字,JavaScript 會默認 this 指向 window 對象。這意味着若是咱們向 window 對象添加 age 屬性並再次調用 sayAge 方法,this.age 將再也不是 undefined 而且變成 window 對象的 age 屬性值。不相信?讓咱們運行這段代碼

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}
複製代碼

很是神奇,不是嗎?這就是第 4 條規則爲何是 window 綁定 的緣由。若是其它規則都沒知足,JavaScript就會默認 this 指向 window 對象。


在 ES5 添加的 嚴格模式 中,JavaScript 不會默認 this 指向 window 對象,而會正確地把 this 保持爲 undefined。

'use strict'

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined
複製代碼

所以,將全部規則付諸實踐,每當我在函數內部看到 this 關鍵字時,這些就是我爲了判斷它的引用而採起的步驟。

  1. 查看函數在哪被調用。
  2. 點左側有沒有對象?若是有,它就是 「this」 的引用。若是沒有,繼續第 3 步。
  3. 該函數是否是用 「call」、「apply」 或者 「bind」 調用的?若是是,它會顯式地指明 「this」 的引用。若是不是,繼續第 4 步。
  4. 該函數是否是用 「new」 調用的?若是是,「this」 指向的就是 JavaScript 解釋器新建立的對象。若是不是,繼續第 5 步。
  5. 是否在「嚴格模式」下?若是是,「this」 就是 undefined,若是不是,繼續第 6 步。
  6. JavaScript 很奇怪,「this」 會指向 「window」 對象。

注:不少小夥伴評論沒有講到箭頭函數,因此譯者專門寫了一篇做爲補充,若有須要瞭解的請挪步也談箭頭函數的 this 指向問題及相關

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索