iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
1
Modern Web

前端開發 30 個問題系列 第 10

"This" in JavaScript

前言
2020 秋天,我將用 30 天的時間,來嘗試回答和網路前端開發相關的 30 個問題。30 天無法一網打盡浩瀚的前端知識,有些問題可能對有些讀者來說相對簡單,不過期待這趟旅程,能幫助自己、也幫助讀者打開不同的知識大門。有興趣的話,跟著我一起探索吧!

What's this?

在物件導向的程式語言當中常常回看到 this 的出現,通常這個 this 會指向物件本身,譬如

const obj = {
  value: 18,
  print: function () {
    console.log(this.value)
  }
}

obj.print()    // 18

但是,好像不是每次使用 this 的時候都能如願以償,如果把上面的 print function 改成 arrow function,就沒辦法得到一樣的值

const obj = {
  value: 18,
  print: () => {
    console.log(this.value)
  }
}

obj.print()     // undefined

所以想趁這個機會,再次釐清一下在 JavaScript 當中關於 this 的用法。不過關於 this 的用法,網路上已經有許多的討論,特別是 Huli 的 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂,內容相當的豐富。所以今天我想反過來,直接從規則和案例出發,幫助自己在實作的時候更能夠上手。

當然,還是建議大家有機會能閱讀相關文章當中的深入討論。

Rules

  1. this 指向的值,跟作用域或在程式碼的哪個位置沒有關係,只跟如何被呼叫有關
  2. 如果找不到被呼叫的對象,那麼 this 就會是
    • 在嚴格模式下,this 為 undefined
    • 在非嚴格模式下,在瀏覽器當中,會是 window
    • 在非嚴格模式下,在 Node.js 當中,會是 global
  • 但是如果遇到 arrow function,則 this 會和「在哪裡被宣告」有關

Cases

以下的例子都是在瀏覽器中執行:

1.

const obj = {
  value: 18,
  print: function () {
    console.log(this)
  }
}

let fn = obj.print
console.log(fn)      // [Function: printer]

obj.print()          // { value: 18, print: [Function: printer] }
fn()                 // window

在這裡我們建立了一個變數 fn 來指向obj.print 這個 function,看起來 fnobj.print 是一模一樣的 function,然而印出來的東西卻完全不一樣。

那是因為在執行 obj.print() 的時候,print function 知道他是被 obj 呼叫,因此當中的 this 會指向 obj。要怎麼知道誰呼叫誰呢?看那個 . 就對了!所以 obj.print() 的意思就是 obj 呼叫 print function 並執行

而當我們在執行 fn() 的時候,因為沒有那個 .,所以找不到可以指向的對象,在找不到的情況下,就會直接指向 window!


2.

const obj = {
  value: 18,
  tool: {
    value: 81,
    print: function() {console.log(this.value)}
  } 
}

let a = obj.tool
let b = obj.tool.print

obj.tool.print()   // 81
a.print()          // 81
b()                // undefined

根據同樣的邏輯,往前找是誰呼叫了這個 function,所以在 obj.tool.print() 這個例子,是 tool 呼叫了 print,所以值是 81。

a.print() 這個例子是 a 呼叫了 print,而 a 本身就是 tool,所以結果一樣是 81。

最後,雖然 b 本身就是 print,但是因為找不到呼叫的對象,所以 this 會指向 window,而因為 window 當中找不到 value 這個值,所以得到的結果是 undefined。


3.

const obj = {
 value: 18,
 print: function () {
   function c () {
     console.log(this)
   }
   c()
 }
}

obj.print()               // window

在這個 case 當中,print function 裡面包含了另外一個 c function,而且這個 c 會直接執行。雖然看起來是因為 print 執行之後 c 跟著執行,所以好像是 print 呼叫了他。但回到剛剛的規則上,c 其實找不到是誰呼叫他的(前面沒有 .),所以在 c 當中的 this 就是 window。

如果今天要讓 c function 當中的 this 指向 obj,可以怎麼做呢?

解法 1: 先把 this 存下來

const obj = {
  value: 18,
  print: function () {
    let self = this
    function c () {
      console.log(self)
    }
    c()
  }
}
obj.print()           // {value: 18, print: [Function]}

解法 2: 變成 IIFE,直接把 this 給傳進去

const obj = {
  value: 18,
  print: function () {
    (function c (self) {
      console.log(self)
    })(this)
  }
}
obj.print()           // {value: 18, print: [Function]}

解法 3: 使用 arrow function

const obj = {
  value: 18,
  print: function () {
    const c = () => console.log(this)
    c()
  }
}
obj.print()           // {value: 18, print: [Function]}

arrow function 的出現使得規則好像變得不太一樣了?沒錯,Arrow function 當中的 this,會跟「它被宣告的地方」有關,而不是跟「它被呼叫的對象」有關。所以因為這裡我們是在 obj 裡面宣告 c,所以 c 當中的 this 就會指向 obj

解法 4: 使用 call, apply, bind

// call
const obj = {
  value: 18,
  print: function () {
    function c(){
      console.log(this)
    }
    c.call(this)
  }
}
obj.print()           // {value: 18, print: [Function]}
// apply
const obj = {
  value: 18,
  print: function () {
    function c(){
      console.log(this)
    }
    c.apply(this)
  }
}
obj.print()           // {value: 18, print: [Function]}

// bind
const obj = {
  value: 18,
  print: function () {
    function c(){
      console.log(this)
    }
    const d = c.bind(this)
    d()
  }
}
obj.print()           // {value: 18, print: [Function]}

使用 JavaScript 當中的 call, apply, bind 方法,綁定 function 當中的 this。三者的差異在於:

  • call: 綁定 this 並可以傳入引數
  • apply: 跟 call 一樣,只是用陣列的方式傳入引數
  • bind: 永久綁定 this。使用的時候會產生新的 function,因此需要傳出來使用。或者當下直接執行。以剛剛的例子來說,就是
// 傳出來使用
const d = c.bind(this)
d()                      // {value: 18, print: [Function]}
// 直接執行
c.bind(this)()           // {value: 18, print: [Function]}

End

希望看完以上的範例之後,下次看到 this 就不會再那麼害怕。不過就算當下不確定,還是可以隨時用 console.log 把 this 印出來看看。今天就簡單先到這邊,我們明天見囉!

Ref


TD
Be curious as astronomer, think as physicist, hack as engineer, fight as baseball player
More about me

"Life is like riding a bicycle. To keep your balance, you must keep moving."



上一篇
Storages in browser
下一篇
JavaScript reduce function
系列文
前端開發 30 個問題31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言