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

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

前言

在深刻研究JavaScript中this關鍵字的細節以前,咱們先退一步想想,爲何this關鍵字存在於第一位。this關鍵字容許你重用具備不一樣上下文的函數。換句話說,"this"關鍵字容許你在調用函數或方法時決定哪一個對象應該是焦點。在此以後咱們談論的一切都將創建在這個想法之上。咱們但願可以在不一樣的上下文中或在不一樣的對象中重用函數或方法。前端

咱們要看的第一件事是如何判斷this關鍵字引用的內容。 當你試圖回答這個問題時,你須要問本身的第一個也是最重要的問題是「這個函數在哪裏被調用?"。你能夠經過查看調用this關鍵字的函數的位置來判斷this關鍵字引用的內容的惟一方法。java

爲了用一個你已經熟悉的例子來證實這一點,好比咱們有一個greet函數,它接受了一個alert消息。數組

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

若是我要問你greet的警告,你的回答是什麼? 只給出函數定義,就不可能知道。 爲了知道name是什麼,你必須看看greet的函數調用。微信

greet('Tyler')
複製代碼

原理是徹底相同的,找出this關鍵字的引用,你甚至,就像你對函數的正常參數同樣 - 它會根據函數的調用方式而改變。app

如今咱們知道爲了弄清楚this關鍵字引用的內容,你必須查看函數定義,讓咱們在實際查看函數定義時創建四個規則來查找。 他們是:函數

  • 隱式綁定
  • 顯式綁定
  • new綁定
  • window綁定

隱式綁定

請記住,這裏的目標是可以使用this關鍵字查看函數定義並告訴this引用的內容。 執行此操做的第一個也是最多見的規則稱爲隱式綁定。 我想說絕大多數狀況它會告訴你this關鍵字引用了什麼。學習

假設咱們有一個看起來像這樣的對象ui

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

如今,若是你要在user對象上調用greet方法,那麼你可使用點表示法。this

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所作的。 .apply.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」對象。

最後

歡迎關注個人微信公衆號【熱前端】,一塊兒交流成長。

相關文章
相關標籤/搜索