1. 程式人生 > >this(他喵的)到底是什麼

this(他喵的)到底是什麼

在深入瞭解 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” 物件。