【譯】理解this及call,apply和bind的用法

kovlento發表於2019-05-10

JavaScript中最容易被誤解的一個方面是this關鍵字。在這篇文章中,將通過學習四個規則來確定此關鍵字引用的內容。隱式繫結,顯式繫結,new繫結和window繫結。在介紹這些時,你還將學習一些其他令人困惑的JavaScript部分,例如.call,.apply,.bind和new關鍵字。

前言

在深入研究JavaScript中this關鍵字的細節之前,我們先退一步想一想,為什麼this關鍵字存在於第一位。this關鍵字允許你重用具有不同上下文的函式。換句話說,"this"關鍵字允許你在呼叫函式或方法時決定哪個物件應該是焦點。在此之後我們談論的一切都將建立在這個想法之上。我們希望能夠在不同的上下文中或在不同的物件中重用函式或方法。

我們要看的第一件事是如何判斷this關鍵字引用的內容。 當你試圖回答這個問題時,你需要問自己的第一個也是最重要的問題是“這個函式在哪裡被呼叫?"。你可以通過檢視呼叫this關鍵字的函式的位置來判斷this關鍵字引用的內容的唯一方法。

為了用一個你已經熟悉的例子來證明這一點,比如我們有一個greet函式,它接受了一個alert訊息。

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

如果我要問你greet的警告,你的回答是什麼? 只給出函式定義,就不可能知道。 為了知道name是什麼,你必須看看greet的函式呼叫。

greet('Tyler')
複製程式碼

原理是完全相同的,找出this關鍵字的引用,你甚至,就像你對函式的正常引數一樣 - 它會根據函式的呼叫方式而改變。

現在我們知道為了弄清楚this關鍵字引用的內容,你必須檢視函式定義,讓我們在實際檢視函式定義時建立四個規則來查詢。 他們是:

  • 隱式繫結
  • 顯式繫結
  • new繫結
  • window繫結

隱式繫結

請記住,這裡的目標是能夠使用this關鍵字檢視函式定義並告訴this引用的內容。 執行此操作的第一個也是最常見的規則稱為隱式繫結。 我想說絕大多數情況它會告訴你this關鍵字引用了什麼。

假設我們有一個看起來像這樣的物件

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

現在,如果你要在user物件上呼叫greet方法,那麼你可以使用點表示法。

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
}
複製程式碼

讓我們來看一個類似但稍微更高階的例子。 現在,我們不僅要擁有名稱,年齡和問候屬性,還要為我們的user物件提供一個mother屬性,該屬性也有名稱和greet屬性。

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}`)
    }
  }
}
複製程式碼

現在問題變成了,下面的每個呼叫會發出什麼alert?

user.greet()
user.mother.greet()
複製程式碼

每當我們試圖弄清楚this關鍵字引用的內容時,我們需要檢視呼叫並看看“左邊的點”是什麼。 在第一次呼叫中,user位於點的左側,這意味著this將引用user。 在第二次呼叫中,mother位於點的左側,這意味著this將引用mother

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

如前所述,絕大多數會有一個“左邊的點”的物件。 這就是為什麼在弄清楚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”是每個函式的一個方法,它允許你呼叫函式,指定呼叫函式的上下文。

考慮到這一點,我們可以使用以下程式碼在user的上下文中呼叫greet

greet.call(user)
複製程式碼

同樣,call是每個函式的屬性,傳遞給它的第一個引數將是呼叫函式的上下文。 換句話說,傳遞給呼叫的第一個引數將是該函式中的this關鍵字引用的內容。

這是規則2(顯式繫結)的基礎,因為我們明確地(使用.call)指定this關鍵字引用的內容。

現在讓我們稍微修改一下greet函式。 如果我們還想傳遞一些引數怎麼辦? 比如:

function greet (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}
複製程式碼

現在,為了將引數傳遞給使用.call呼叫的函式,在指定第一個作為上下文的引數後,將它們逐個傳遞給它們。

function greet (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}

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

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

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

它顯示瞭如何將引數傳遞給使用.call呼叫的函式。 但是,正如你可能已經注意到的那樣,必須從我們的languages陣列中逐個傳遞引數,這有點令人討厭。 如果我們可以將整個陣列作為第二個引數傳入並且JavaScript會將它們傳播給我們,那將是很好的。 對我們來說這是個好訊息,這正是.apply所做的。 .appall.call完全相同,但不是逐個傳入引數,而是傳入一個陣列,它會將這些資料作為函式中的引數傳遞出去。

所以現在使用.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 (l1, l2, l3) {
  alert(
    `Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
  )
}

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) {
  /*
    Under the hood, JavaScript creates a new object called `this`
    which delegates to the User's prototype on failed lookups. If a
    function is called with the new keyword, then it's this new object
    that interpretor created that the this keyword is referencing.
  */

  this.name = name
  this.age = age
}

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

詞法繫結

你已經聽說過並且之前使用過箭頭函式。 那是ES6的新版本, 以更簡潔的格式編寫函式。

friends.map((friend) => friend.name)
複製程式碼

除了簡潔之外,箭頭函式在涉及this關鍵字時具有更直觀的方法。 與普通函式不同,箭頭函式沒有自己的this。 相反,這是詞法決定的。 這是一種奇特的方式,說明this是根據正常的變數查詢規則確定的。 讓我們繼續我們之前使用的例子。 現在,讓我們將它們組合起來,而不是讓languagegreet與物件分開。

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {}
}
複製程式碼

之前我們假設languages陣列的長度總是為3.通過這樣做,我們可以使用硬編碼變數,如l1l2l3。 我們讓greet更靈活一點,並假設languages可以是任意長度。 所以,我們將使用.reduce來建立字串

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce(function (str, lang, i) {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }, "")

    alert(hello + langs)
  }
}
複製程式碼

雖然程式碼多了,但最終結果應該是相同的。 當我們呼叫user.greet()時,我們希望看到Hello, my name is Tyler and I know JavaScript, Ruby, and Python..可悲的是,有一個錯誤。 你發現l 嗎? 抓取上面的程式碼並在控制檯中執行它。 你會注意到它正在丟擲錯誤Uncaught TypeError: Cannot read property 'length' of undefined.。 我們只在第9行使用了.length,所以我們知道我們的錯誤就在那裡。

if (i === this.languages.length - 1) {}
複製程式碼

根據我們的錯誤,this.langauges是未定義的。 讓我們通過我們的步驟來弄清楚這個關鍵字引用的原因是什麼,它應該是不引用use的。 首先,我們需要檢視呼叫函式的位置。 等等? 被呼叫的函式在哪裡? 該函式正被傳遞給.reduce,所以我們不知道。 我們從未真正看到過我們的匿名函式的呼叫,因為JavaScript在.reduce的實現中就是這樣做的。 那就是問題所在。 我們需要指定我們希望傳遞給.reduce的匿名函式在使用者的上下文中呼叫。 這樣this.languages將引用user.languages。 如上所述,我們可以使用.bind

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce(function (str, lang, i) {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }.bind(this), "")

    alert(hello + langs)
  }
}
複製程式碼

所以我們已經看到.bind如何解決這個問題,但這與箭頭函式有什麼關係。 之前我說用箭頭功能this是詞法決定的。

在上面的程式碼中,遵循你的自然直覺,this關鍵字引用匿名函式內部會是什麼? 對我來說,它應該引用use。 沒有理由建立一個新的上下文因為我必須將一個新函式傳遞給.reduce。 憑藉這種直覺,箭頭功能經常被忽視。 如果我們重新編寫上面的程式碼,除了使用匿名箭頭函式而不是匿名函式宣告之外什麼都不做,一切都“正常”。

const user = {
  name: 'Tyler',
  age: 27,
  languages: ['JavaScript', 'Ruby', 'Python'],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce((str, lang, i) => {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }, "")

    alert(hello + langs)
  }
}
複製程式碼

再次出現這種情況的原因是因為使用箭頭功能,this是“詞法上”確定的。 箭頭功能沒有自己的this。 相反,就像使用變數查詢一樣,JavaScript直譯器將檢視(父)作用域以確定this引用的內容。

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 age is undefined的,因為this.age將是未定義的。 這裡的事情變得瘋狂了。這裡真正發生的是因為點的左邊沒有任何內容,我們沒有使用.call.apply.bindnew關鍵字,JavaScript預設this引用window物件。 這意味著如果我們將一個age屬性新增到window物件,那麼當我們再次呼叫我們的sayAge函式時,this.age將不再是未定義的,而是它將是window物件上的age屬性。 不相信我? 執行此程式碼,

window.age = 27

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

非常粗糙,對嗎? 這就是為什麼第5個規則是window繫結。 如果沒有滿足其他規則,則JavaScript將預設this關鍵字引用window物件。

在ES5中新增的嚴格模式中,JavaScript將做正確的事情,而不是預設為window物件只是將“this”保持為未定義。

'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”關鍵字未定義。如果沒有,繼續#6

  6. 是的,JavaScript很奇怪。 “this”引用了“window”物件。

最後

歡迎關注我的微信公眾號【熱前端】,一起交流成長。

【譯】理解this及call,apply和bind的用法

相關文章