重新理解 this 的值

人稱 JS 的頭號公敵。

this 最原始的用途

回歸到原點,其實 this 本身就是為了物件導向而存在的東西,用來指向你建立出來的 instance

例如說:

1
2
3
4
5
6
7
8
9
10
11
class Dog {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}

const dog1 = new Dog('PeaNu')
console.log(dog1.getName()) // PeaNu

首先,我透過 Dog 建立了一個 instance:dog1

記住,只要是出現在 dog1 裡面的 this 都一律代表它自己,不會有任何例外情形發生。在物件導向下的 this 就是這麼單純,沒有一大堆莫名其妙的情況發生。

所以我一開始給 dog1 的 name 是 PeaNu,那麼它裡面的 this.name 就只會是 PeaNu。

之後如果又建立了另一個 dog2,把它的 name 設為 PPB,那它的 this.name 也只會是 PPB,跟 dog1 一點關係也沒有。

總而言之,this 的初衷就是用來代表「這個 instance」的意思,非常非常非常單純。

在非物件導向的環境下,this 的值沒有意義

如果你硬要在不是物件導向的地方用 this,就會出現一些奇怪的結果:

1
2
3
4
function test() {
console.log(this)
}
test() // window / global / undefind (strict mode)

在這種情況下,this 就會根據不同的環境而有不同的值。

  1. undefined,在使用嚴格模式下的預設值
  2. window,在瀏覽器下的預設值
  3. global 在 Node.js 下的預設值

不管最後的值是什麼,這種 this 都沒什麼意義,所以才會有這個標題。

想知道 this 值,得看是怎麼呼叫的,不是宣告

舉個例子:

1
2
3
4
5
6
7
8
9
10
'use strict'
const obj = {
a: 'yoyo',
test: function () {
console.log(this)
}
}
obj.test() // obj
const func = obj.test
func() // undefined

同樣都是呼叫 test,但第一個結果是 obj,第二個結果是 undefined,為什麼?因為呼叫的方式不同。

第一個 test 是透過 obj 來呼叫的,而第二個是直接執行 func,這兩種的呼叫方式是不一樣的。所以儘管 test 是宣告在 obj 這個物件裡,但只要用不同的方式來呼叫它,this 的值就會不同。

再次強調:

  • 重點是怎麼呼叫,而不是宣告
  • 重點是怎麼呼叫,而不是宣告
  • 重點是怎麼呼叫,而不是宣告

所以再出一題來考考你,下面的 this 值會是什麼?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use strict'

const obj = {
a: 'obj',
test: function () {
console.log(this)
}
}
const obj2 = {
a: 'obj2',
test2: obj.test
}

obj2.test2() // ???

想完後就貼到 console,看跟自己想的一不一樣。

透過 call、apply 和 bind 來改變 this 值

既然 this 值會變來變去的,那有沒有辦法控制它?

有,就用 callapplybind 來控制,舉個範例:

1
2
3
4
5
6
7
8
9
'use strict'

function test() {
console.log(this)
}

test() // undefined
test.call('this is call') // this is call
test.apply('this is apply') // this is apply

callapply 是另一種執行 function 的方式,跟 () 的差別在於它們可以傳入一個參數,這個參數就是用來指定 this 的值,你傳什麼進去就會出來什麼。

至於 bind 比較特別一點,一樣先看例子:

1
2
3
4
5
6
7
8
9
'use strict'
function test() {
console.log(this)
}

const bindFunc = test.bind('this is bind')
bindFunc() // this is bind
bindFunc.call('is this call?') // this is bind
bindFunc.apply('is this apply?') // this is bind

首先 bind 的作用不是用來呼叫 function,而是把綁定 this 後的 function 給回傳,以上面的例子來說就是 bindFunc

這時候你再用 callapply 來呼叫也沒有用,this 值只會是一開始綁定的那個值,不能被改變。

一種快速判斷 this 值的技巧

在知道 call 怎麼使用以後,你就可以用這種角度來思考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict'

const obj = {
a: 'obj',
test: function () {
console.log(this)
}
}

obj.test() // obj
obj.test.call(obj) // obj

const func = obj.test
func() // undefined
func.call(undefined) // undefined

透過這種把 function 前面的東西丟到 call 裡面,會幫助你更好判斷 this 值是什麼。不過還是要強調一下,這只是方便記憶,也許在 90% 的情境下是正確的,但不要忘了還是有 10% 的可能是錯的。

例外狀況

事件監聽器

在事件監聽下的 this 值會是被綁定的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul>
<li><button>click</button></li>
<li><button>click</button></li>
<li><button>click</button></li>
<li><button>click</button></li>
</ul>

<script>
document.querySelector('ul').addEventListener('click', function (e) {
console.log(this) // ul
console.log(this === e.targer) // false
})
</script>

這裡綁定的是 ul,所以每當觸發 click 時,this 值就會是 ul 這個元素。(注意不是 e.target

箭頭函式

注意:這邊的範例是以 Node.js 為主,如果是瀏覽器的話結果可能不太一樣。

宣告的那個地方 this 值是什麼,出來就是什麼。

箭頭函式跟一般函式差別最大的地方就是這裡,它只在意「宣告」的地方,不會管「呼叫方式」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict'

const obj = {
a: 'obj',
whatIsThis: this,
test1: function () {
console.log('normal function: ', this)
},
test2: () => {
console.log('arrow function:', this)
}
}

console.log(obj.whatIsThis) // {}
obj.test1() // obj 自己
obj.test2() // {}

一個一個來看,首先在 obj 裡定義了 whatIsThis 來確認裡面的 this 值是什麼,得到的結果是 {}

接著呼叫 test1,按照前面所說,一般函式的 this 值會根據呼叫的方式來決定 this 值,而這裡是透過 obj 來呼叫的,所以 this 值就是 obj 本身,符合推論。

再來是 test2,也如同前面所說,箭頭函式的 this 值只管宣告的地方,所以宣告的地方是 obj 裡面,而 obj 裡的 this 值是 {},所以最後的結果確實是 {}

最後再出一題考考你,答得出來就代表你能分辨箭頭函式和一般函式的差別了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict'

const obj = {
a: 'obj',
whatIsThis: this,
test1: function () {
console.log('normal function: ', this)
},
test2: () => {
console.log('arrow function:', this)
},
test3: function () {
console.log('outter normal function:', this)
setTimeout(() => {
console.log('inner arrow function', this)
}, 1000)
}
}

console.log(obj.whatIsThis) // {}
obj.test1() // obj 自己
obj.test2() // {}
obj.test3() // ???
mentor-program-day95 物件導向之繼承
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×