- 原文地址:WTF is this - Understanding the this keyword, call, apply, and bind in JavaScript
- 原文做者:Tyler McGinnis
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:CoolRice
- 校對者:周家未
JavaScript 中最容易被誤解的一點就是 this
關鍵字。在這篇文章中,你將會了解四種規則,弄清楚 this
關鍵字指的是什麼。隱式綁定、顯式綁定、new 綁定和 window 綁定。在介紹這些技術時,你還將學習一些 JavaScript 其餘使人困惑的部分,例如 .call
、.apply
、.bind
和 new
關鍵字。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
請記住,這裏的目標是查看使用 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
}
複製代碼
咱們來看一個相似但稍微高級點的例子。如今,咱們的對象不只要擁有 name
、age
和 greet
屬性,還要被添加一個 mother
屬性,而且此屬性也擁有 name
和 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}`)
}
}
}
複製代碼
如今問題變成下面的每一個函數調用會警告什麼?
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"
複製代碼
第三條判斷 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)
複製代碼
假如咱們有下面這段代碼
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
關鍵字時,這些就是我爲了判斷它的引用而採起的步驟。
注:不少小夥伴評論沒有講到箭頭函數,因此譯者專門寫了一篇做爲補充,若有須要瞭解的請挪步也談箭頭函數的 this 指向問題及相關。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。