- 原文地址: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
關鍵字。
視訊
- YouTube 視訊連結:youtu.be/zE9iro4r918
正文
在深入瞭解 JavaScript 中的 this
關鍵字之前,有必要先退一步,看一下為什麼 this
關鍵字很重要。this
允許複用函式時使用不同的上下文。換句話說,“this” 關鍵字允許在呼叫函式或方法時決定哪個物件應該是焦點。 之後討論的所有東西都是基於這個理念。我們希望能夠在不同的上下文或在不同的物件中複用函式或方法。
我們要關注的第一件事是如何判斷 this
關鍵字的引用。當你試圖回答這個問題時,你需要問自己的第一個也是最重要的問題是“這個函式在哪裡被呼叫?”。判斷 this
引用什麼的 唯一 方法就是看使用 this
關鍵字的這個方法在哪裡被呼叫的。
用一個你已經十分熟悉的例子來展示這一點,比如我們有一個 greet
方法,它接受一個名字引數並顯示有歡迎訊息的警告框。
function greet (name) {
alert(`Hello, my name is ${name}`)
}
複製程式碼
如果我問你 greet
會具體警告什麼內容,你會怎樣回答?只給出函式定義是不可能知道答案的。為了知道 name
是什麼,你必須看看 greet
函式的呼叫過程。
greet('Tyler')
複製程式碼
判斷 this
關鍵字引用什麼也是同樣的道理,你甚至可以把 this
當成一個普通的函式引數對待 — 它會隨著函式呼叫方式的變化而變化。
現在我們知道為了判斷 this
的引用必須先看函式的定義,在實際地檢視函式定義時,我們設立了四條規則來查詢引用,它們是
- 隱式繫結
- 顯式繫結
- new 繫結
- window 繫結
隱式繫結
請記住,這裡的目標是檢視使用 this
關鍵字的函式定義,並判斷 this
的指向。執行繫結的第一個也是最常見的規則稱為 隱式繫結
。80% 的情況下它會告訴你 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
}
複製程式碼
我們來看一個類似但稍微高階點的例子。現在,我們的物件不僅要擁有 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"
複製程式碼
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
關鍵字時,這些就是我為了判斷它的引用而採取的步驟。
- 檢視函式在哪被呼叫。
- 點左側有沒有物件?如果有,它就是 “this” 的引用。如果沒有,繼續第 3 步。
- 該函式是不是用 “call”、“apply” 或者 “bind” 呼叫的?如果是,它會顯式地指明 “this” 的引用。如果不是,繼續第 4 步。
- 該函式是不是用 “new” 呼叫的?如果是,“this” 指向的就是 JavaScript 直譯器新建立的物件。如果不是,繼續第 5 步。
- 是否在“嚴格模式”下?如果是,“this” 就是 undefined,如果不是,繼續第 6 步。
- JavaScript 很奇怪,“this” 會指向 “window” 物件。
注:很多小夥伴評論沒有講到箭頭函式,所以譯者專門寫了一篇作為補充,如有需要了解的請挪步也談箭頭函式的 this 指向問題及相關。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。