3 分鐘閱讀

this你搞得我好亂啊!

又過了快一週啦!終於要開始寫部落個的第一篇技術相關文章了,這次決定紀錄一下學習 JavaScript 後,在撰寫程式碼要使用this時,大腦裡想像的 this 時不時跟console.log(this)出來的 this 不一樣,變成在 coding 時往往不會想到要拿 this 來使用。然而現實總是殘忍的,自己的 code 可以避開,但閱讀其他人的程式碼總躲不掉的啊!為了往後增加撰寫、閱讀程式碼時碰到 this 能夠更有效率、更快速辨識出 this 是誰,這次就來探討一下 this 在哪些情況下究竟他的真面目是誰!

This 是什麼?

引用自 MDN 上 this 的定義

A function’s this keyword behaves a little differently in JavaScript compared to other languages. It also has some differences between strict mode and non-strict mode.

In most cases, the value of this is determined by how a function is called (runtime binding). It can’t be set by assignment during execution, and it may be different each time the function is called.

簡單的翻譯如下:

在 JavaScript 的 this 與其他的程式語言有些微的不同,JavaScirpt 的嚴格模式和非嚴格模式下的 this 也會有不一樣的結果。

在大多數情況下,this 會因為呼叫 function 的方式而有不同的值,並且不能夠在執行階段指定一個值給 this。

看完 MDN 的解釋後我的疑惑更多了!是哪裡跟其他程式語言不一樣呢?什麼又是嚴格/非嚴格模式?還有大多數的情況會因為呼叫 function 有不同的 this,那麼少數的情況又是什麼呢?讓我們一步一步的看看在JavaScript中有哪幾種地方常出現this。

物件導向中的 this

在某些程式語言中的使用物件導向時,this 指的就是是產生出的實體(instance),當然在 JavaScript 中使用物件導向時的 this 也是指向產生出的 instance,例如:

1
2
3
4
5
6
7
8
9
10
class Student {
  constructor(name, number, major) {
    this.name = name;
    this.number = number;
    this.major = major;
  }
}

const tgo = new Student("Tgo", 1, ["OT"]);
console.log(tgo); // Student { name: 'Tgo', number: 1, major: ['OT'] }

這個例子中 this 指的就是利用new方法產生出來的實體,可以使用 this 很快的定義 instance 擁有的屬性以及方法。而特別的是 JavaScript 是使用 Prototype 的概念來完成物件導向的,這個例子可以使用 class 來撰寫是因為 ES6 之後提供了 class 寫法的 syntax sugar,ES6 之前只能夠使用prototype的寫法來實作物件導向的概念。

還有另外一種跟物件有關的 this,就是我們直接在物件內使用 this。這個時候的物件跟前一個物件導向產生出的 instance 實體物件有些微的不同,這時候的物件是我們直接產生出來的物件,不是透過物件導向的 new 產生出來的 instance,例如:

1
2
3
4
5
6
7
8
9
10
const tgo = {
  name: "tgo",
  major: ["OT"],
  chooseMajor: function (newMajor) {
    this.major.push(newMajor);
  },
};

tgo.chooseMajor("Italian Cusine");
console.log(tgo.major); // ['OT', 'Italian Cusine']

一個物件內使用到的 this,指的就是呼叫 this 的物件(也就是自己),所以我們可以直接用 this 取得物件內的屬性或是方法。

從這兩個例子可以發現,在物件導向或物件內可以很快速地理解 this 指的是誰。不過當離開了這裡之後就會常常碰到我們想像以外 this 出現!

物件導向外呼叫的 this

接下來探討我們在物件以外的地方呼叫的 this 到底是誰,在 JavaScript 中可以在 function 內使用 this,例如:

1
2
3
4
5
function whatIsThis() {
  console.log(this);
}

whatIsThis();

這個時候的 this 讓人有點摸不照頭緒了!雖然已經知道 JavaScript 中 function 也是一個 object,但這裡的 this 卻不是指 function 本身。

那麼這個 this 到底是什麼呢?根據前面 MDN 的描述“this 會因為不同呼叫 function 的方式而有不同的值”,funciton 內的 this 會因為是誰呼叫 function 而指向不同的對象。這個例子中console.log出來的結果在不同執行的環境下會有不同的結果:

  • 嚴格模式下:this 會是undefined
  • 非嚴格模式下:this 會是全域物件,所以不同地方執行 JavaScript 全域物件有所不同
    • 瀏覽器環境:this 指的是Window物件
    • node.js 環境:this 指的是global物件

看到這裡開始眼花我想是正常現象。在前一個例子物件的方法也是在 function 內呼叫 this,怎麼現在 this 卻變成了全域物件或是undefined?這時候就要注意到底是誰在呼叫 function,前面物件內方法的例子我們是用tgo物件來呼叫他的方法,因此 this 就會是tgo物件本身。而這個例子中呼叫whatIsThis這個 function 的因為沒有特別指出是哪個物件,所以就會有上述三種不同的 this 出現。

另一種常常會出現 this 的地方在操作 DOM 元素時,添加事件會用到的 event handler,讓我們來看看例子:

1
2
3
4
document.querySelector("button").addEventListener("click", function (e) {
  console.log(this);
  console.log(e.currentTarget);
});

當我們每次點擊按鈕時console.logthise.currentTarget,可以從結果看到 this 就是我們註冊事件的 DOM 元素,也就是e.currentTarget。目前我會都只有在使用 event delegation 概念時,才會在 event handler 內會用到 this,省得去打e.currentTarget漏漏長的字。

自己決定 this 到底是誰

後面三個例子中可以發現,同樣是在 function 內使用了 this,但不同情況 this 指的東西不一樣。前面幾個例子中 this 的值由以下兩個綁定方式來決定:

  • 預設綁定(Default Binding):沒有特別指定哪個物件呼叫 function,就是指向全域物件或undefined
  • 隱含式綁定(Implicit Binding):指向呼叫 function 的物件

除了這兩種綁定方法以外,我們也可以使用顯式綁定(Explicit Binging)自己決定 this 到底是誰!也就是使用callapplybind三種 function 來綁定 this,其中callapply是另一種呼叫 function 的方法,與平常使用()呼叫 function 不同在於執行 function 前可以自己綁定 thisˋ 指向哪個物件。bind則可以強制綁定 function 的 this,並且回傳新的綁定好 this 的 function。來看個簡單的例子要怎麼使用這三個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const tgo = {
  name: "Tgo",
  greetingWord: "Hi",
};

function greeting(who) {
  console.log(`${this.greetingWord}, ${who}`);
}

greeting("How are you?"); // "undefined, How are you?""

tgo.greeting = greeting.bind(tgo);
tgo.greeting("there"); // "Hi, there"

greeting.call(tgo, "everybody"); // "Hi, everybody"
greeting.apply(tgo, ["everyone"]); // "Hi, everyone

第 10 行可以看到當我們直接呼叫greeting方法時因為全域物件內沒有greetingWord屬性,所以前面會印出undefined

第 12、13 行將greeting方法的 this 強制指定為tgo,並且將回傳的 function 指定給tgo.greeting屬性,這樣tgo內就有個一個打招呼的方法可以使用。

最後第 15、16 行一樣在全域環境下呼叫greeting,但是因為使用了callapply方法將 this 綁定成tgo,所以印出的打招呼方式就不會是undefined了!

至於callapply的差別又是什麼?我想給我自己挖個坑,之後寫個簡單的文章解釋一下。

arrow funciton 中的 this

最後來講講在 arrow function 中的 this 指的是誰,它與普通宣告的 function 不太一樣。在呼叫 arrow function 之前會將 this 強制綁定,並且在之後不能夠使用'use strict'bind重新綁定 this 的值,來簡單看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const tgo = {
  name: "Tgo",
  sayHi: function () {
    console.log(`Hi, My name is ${this.name}.`);
    const arrowFunction = () => {
      console.log(`arrow function的this是:${this}`);
    };

    arrowFunction();
  },
};

tgo.sayHi(); // "Hi, My name is Tgo."
const arrowFunctionThis = tgo.sayHi;
arrowFunctionThis(); // "Hi, My name is undefined."

可以看到第 13 行由tgo呼叫sayHi時 this 指的是tgo自己,但是第 15 行呼叫arrowFunctionThis時沒有指定是哪個物件進行呼叫,所以會是全域物件或是嚴格模式下的undefined。此外當嚴格模式下 this 是undefined時會產生錯誤,因為第 4 行的位置呼叫了 name 屬性,而undefined根本沒有 name 這個屬性。

另外 event handler 如果是使用 arrow function 撰寫,取得的 this 會是全域物件,並且因為已經強制綁定過的關係沒辦法再使用bind重新綁定 this 的值,所以在事件內會使用到 this 時我還是使用 function declration 來撰寫。

Asynchronous Callback 的 this

在 AJAX 或是setTimeoutsetInterval等等非同步處理的 callback function 內呼叫 this 時,this 會是呼叫 callback function 的物件,大多數情況下會是全域物件。因此可以使用bind方法先把 callback 的 this 綁定到原本呼叫 asynchronous function 的物件上。當然也可以先將呼叫 asynchronous function 的物件其中的 this 放到一個變數內,然後在 callback 內就可以使用該變數取得原本物件的 this,但個人認為還是使用bind程式碼看起來比較好閱讀一點。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function asyncCallback() {
  const that = this;

  setTimeout(function () {
    console.log(that.name);
  }, 1000);
}

function boundAsyncCallback() {
  setTimeout(
    function () {
      console.log(this.name);
    }.bind(tgo),
    1000
  );
}

var name = "全域物件";
const tgo = {
  name: "Tgo",
  asyncCallback: asyncCallback,
  boundAsyncCallback: boundAsyncCallback,
};

asyncCallback(); // "全域物件"
tgo.asyncCallback(); // "Tgo"

boundAsyncCallback(); // "Tgo"
tgo.boundAsyncCallback(); // "Tgo"


舉了這麼漏漏長沒什麼實際應用的例子,但對於自己判斷 this 什麼時候指的是誰有更深刻的印象。最後來總結一下判斷 this 的方法:

  • 在物件導向內的 this 就會是產生出來的 instance
  • 在物件導向外的 this 則是呼叫 function 的物件,如果沒有指定是誰呼叫則是全域物件或undefined
  • 呼叫 function 前可以使用bindcallapply決定 this 是誰
  • arrow function 內呼叫的 this 就看宣告 arrow function 時該作用域的 this 是什麼
  • callback function 內的 this 會指向呼叫 callback 的物件是誰(大多是全域物件)


此篇文主要目的在記錄學習程式語言時的筆記,如果有錯誤之處請不吝於私訊或來信指教,我會很感謝您給的回饋! 以上內容參考自下列網站/文章

  1. this - JavaScript | MDN
  2. 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂
  3. What’s THIS in JavaScript?

留言