[譯] JavaScript中的“this”是什麼?

騰訊IVWEB團隊發表於2018-08-05

原文:What is “this” in JavaScript?

如果你曾使用JavaScript庫做過開發,那麼你可能已經注意到一個名為 this的特定關鍵字。雖然 this在JavaScript中非常常見,但是完全理解this關鍵字的原理以及在程式碼中如何使用對相當一部分的開發者來說著實不易。在這篇文章中,我將幫你深入理解 this及其工作機制。

在開始之前,請確保已安裝 Node。然後,開啟命令列終端並執行 node 命令。

全域性環境中的“this”

this的工作機制理解起來並不是很容易。我們通過將 this置於不同環境下,分別來理解 this是如何工作的。首先看一下 global環境。

在全域性環境中, this相當於全域性物件 global

> this === global
true
複製程式碼

但這隻在 node中才有效。如果我們將相同的程式碼放在js檔案中執行,得到的輸出為false。

如果想測試效果,可以建立一個名為 index.js的檔案,包含以下程式碼:

console.log(this === global);
複製程式碼

然後使用 node命令執行此檔案:

$ node index.js
false
複製程式碼

原因是在JavaScript檔案中, this等同於 module.exports而不是 global

函式中的“this”

函式中的 this值通常是由函式的呼叫方來定義。因此,每次執行函式,函式內的 this值可能都不一樣。

index.js檔案中,編寫一個非常簡單的函式,來檢查 this是否等於global物件。

function rajat() {
  console.log(this === global)
}
rajat()
複製程式碼

如果我們使用 node執行此程式碼,得到的輸出為 true 。如果在檔案的頂部新增 "use strict",並再次執行它,將會得到一個 false輸出,因為現在的 this值為 undefined 。

為了進一步解釋這一點,讓我們建立一個簡單的函式來定義超級英雄的真實姓名和英雄名字。

function Hero(heroName, realName) {
  this.realName = realName;
  this.heroName = heroName;
}
const superman= Hero("Superman", "Clark Kent");
console.log(superman);
複製程式碼

請注意,這個函式並不是以嚴格模式編寫的。 node執行此程式碼,並不會得到我們預期的“Superman”和“Clark Kent”,但它只會給我們一個 undefined 。

背後的原因是,由於函式不是以嚴格模式編寫的, this指向global物件。

如果在嚴格模式下執行此程式碼,我們會收到報錯,因為JavaScript不允許將屬性 realNameheroName賦給 undefined 。其實這是一件好事,因為它阻止我們建立全域性變數。

另外,以大寫形式書寫函式名意味著我們需要將它視為建構函式,使用 new運算子來呼叫。用下面的程式碼替換上面程式碼段的最後兩行:

const superman = new Hero("Superman", "Clark Kent");
console.log(superman);
複製程式碼

再次執行 node index.js命令,現在會得到你預期的輸出。

建構函式中的“this”

JavaScript沒有特定的建構函式。我們所能做的就是使用 new運算子將函式呼叫轉換為建構函式呼叫,如上一節所示。

建構函式被呼叫時,會建立一個新物件並將其設定為函式的 this引數。建構函式會隱式的返回這個物件,除非我們明確的返回了另外一個物件。

hero函式內部新增下面的 return語句:

return {
  heroName: "Batman",
  realName: "Bruce Wayne",
};
複製程式碼

如果我們再次執行 node命令,我們會看到 上面的 return語句覆蓋了建構函式呼叫。

return語句不會覆蓋建構函式呼叫的唯一情形是,return語句返回的不是一個物件。在這種情況下,物件將包含原始值。

方法中的“this”

當函式作為物件的方法被呼叫時, this指向的是該物件,也稱為函式呼叫的接收器(receiver)。

假設 hero物件有一個 dialogue方法 ,那麼 dialogue中的 this值指向 hero本身。此時的 hero也被稱為 dialogue方法呼叫的接收者。

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}!`);
  }
};
hero.dialogue();
複製程式碼

這個例子非常簡單,但在實際情況中,我們的方法很難跟蹤接收器。在 index.js的末尾新增以下程式碼段。

const saying = hero.dialogue;
saying();
複製程式碼

如果我將 dialogue的引用儲存在另一個變數中,並將該變數作為函式呼叫。 node執行程式碼 , this將返回 undefined ,因為該方法已經丟失了接收器。 this此時指向 global ,而不是 hero 。

當我們將一個方法作為回撥傳遞給另一個方法時,通常會丟失接收器。我們可以通過新增包裝函式或使用 bind方法將 this與特定物件繫結來解決這個問題。

call() 和 apply()

雖然函式的 this值是隱式設定的,我們在呼叫函式時也可以使用 call()和 apply()明確設定 this引數。

我們重構一下前面章節的程式碼片段,如下所示:

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
複製程式碼

如果要將 hero物件作為 dialogue函式的接收器,我們可以這樣使用 call()或 apply() :

dialogue.call(hero)
// or
dialogue.apply(hero)
複製程式碼

如果你是在非嚴格模式下使用 call或 apply ,JavaScript引擎會忽略傳遞給 call或 apply的 null或 undefined (譯者注:被替換為global)。這也是為什麼建議始終以嚴格模式編寫程式碼的原因之一。

bind()

當我們將一個方法作為回撥函式傳遞給另一個函式時,總是存在丟失方法的原有接收器的風險,使得 this引數指向全域性物件。

bind()方法可以將 this引數固定的繫結到一個值上。下面的程式碼片段, bind會建立一個新的 dialogue函式,並將 this值設定為 hero 。(譯者注:bind()方法會建立一個新函式,稱為繫結函式-bound function-BF,當呼叫這個繫結函式時,繫結函式會以建立它時傳入 bind()方法的第一個引數作為 this,傳入 bind() 方法的第二個以及以後的引數加上繫結函式執行時本身的引數按照順序作為原函式的引數來呼叫原函式。)

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}`);
  }
};
setTimeOut(hero.dialogue.bind(hero), 1000);
複製程式碼

這樣的話,即使使用 call或 apply方法也無法改變 this的值 。

箭頭函式中的“this”

箭頭函式內的 this與其他型別的JavaScript函式有很大的不同。An arrow function uses the this value from its enclosing execution context, since it does have one of its own.

箭頭函式會永久地捕獲 this值,阻止 apply或 call後續更改它。

為了解釋箭頭函式中的 this是如何工作的,我們來寫一個箭頭函式:

const batman = this;
const bruce = () => {
  console.log(this === batman);
};
bruce();
複製程式碼

這裡,我們將 this值儲存在變數中,然後將該值與箭頭函式內的 this值進行比較。在終端中執行 node index.js,輸出應該為 true 。

箭頭函式內的 this值無法明確設定。此外,使用 call 、 apply或 bind等方法給 this傳值,箭頭函式會忽略。箭頭函式引用的是箭頭函式在建立時設定的 this值。(譯者注:箭頭函式中沒有this繫結,必須通過查詢作用域鏈來決定它的值,如果箭頭函式被非箭頭函式包裹,那麼this值由外圍最近一層非箭頭函式決定,否則為undefined。)

箭頭函式也不能用作建構函式。因此,我們也不能在箭頭函式內給 this設定屬性。

那麼箭頭函式對 this 可以做什麼呢?

箭頭函式可以使我們在回撥函式中訪問 this 。通過下面的 counter物件來看看如何做到的:

const counter = {
  count: 0,
  increase() {
    setInterval(function() {
      console.log(++this.count);
    }, 1000);
  }
}
counter.increase();
複製程式碼

使用 node index.js執行此程式碼,只會得到一個 NaN的列表。這是因為 this.count已經不是指向 counter物件了。它實際上指向的為 global物件。

如果想讓計數器正常工作,可以使用箭頭函式重寫它。

const counter = {
  count: 0,
  increase () {
    setInterval (() => {
      console.log (++this.count);
    }, 1000);
  },
};
counter.increase();
複製程式碼

回撥函式使用 this與 increase方法繫結, counter現在可以正常工作了。

注意 :不要將 ++this.count 寫成 this.count + 1。後者只會增加count的值一次,每次迭代都會返回相同的值。

Class中的“this”

類是JavaScript應用程式中最重要的一部分。讓我們看看類中的this有何不同。

類通常包含一個 constructor , this可以指向任何新建立的物件。

不過在作為方法時,如果該方法作為普通函式被呼叫, this也可以指向任何其他值。與方法一樣,類也可能失去對接收器的跟蹤。

我們將之前建立的 Hero函式改造為類。該類包含一個建構函式和一個 dialogue()方法。最後,建立一個類的例項並呼叫 dialogue方法。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();
複製程式碼

建構函式裡的 this指向新建立的 類例項。當我們呼叫 batman.dialogue()時, dialogue()作為方法被呼叫, batman是它的接收器。

但是如果我們將 dialogue()方法的引用儲存起來,並稍後將其作為函式呼叫,我們會丟失該方法的接收器,此時 this引數指向 undefined 。

const say = batman.dialogue;
say();
複製程式碼

出現錯誤的原因是JavaScript 類是隱式的執行在嚴格模式下的。我們是在沒有任何自動繫結的情況下呼叫 say()函式的。要解決這個問題,我們需要手動使用 bind()將 dialogue()函式與 batman繫結在一起。

const say = batman.dialogue.bind(batman);
say();
複製程式碼

我們也可以在 建構函式方法中做這個繫結。

總結

我們需要在JavaScript中使用 this ,就像我們需要在英語中使用代詞一樣。以這兩句話為例:

  • Rajat loves DC Comics.
  • Rajat also loves Marvel movies.

我們可以使用代詞將這兩個句子組合在一起,所以這兩句話現在成了:

Rajat loves DC Comics, and he also loves Marvel Comics

這個簡短的語法課完美地解釋了 this在JavaScript中的重要性。就像代詞 he將兩個句子連線在一起一樣, this可以作為再次引用相同內容的捷徑。

希望這篇文章可以幫助你解答JavaScript中有關 this的困惑,輕鬆駕馭這個簡單但非常重要的關鍵字。


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章