JavaScript “this” 解釋與說明
過去一段時間的我,每次看到 JavaScript 的 this
總會眉頭一皺,雖然大多時候只要把 console.log(this);
寫到程式碼,就大概知道 this
指的是哪個東西。不過知道歸知道這時 this
指的是什麼,還是不了解為什麼。
我曾經對於 this
的誤解
請你看一下這個程式碼,看看能不能解釋他。
function foo() {
console.log(this.a);
}var a = 1;foo(); // -> 1
恩,看來這時的 this
指的是 global (window) 物件。這時以前的我曾經錯誤的以為 this
指的就是目前 scope 外面那層。
function foo() {
console.log(this.a);
}var a = 1;
var obj = {
a: 2,
foo: foo,
};obj.foo(); // -> 2
這時候以前的我就搞不太懂為什麼是印出 2 了,我錯誤的概念告訴我如果是被裝在某物件裡的函數,就會使用該物件當作 scope 來當作 this 。
function foo() {
console.log(this.a);
}var a = 1;
var obj = {
a: 2,
foo: foo,
};obj.foo(); // -> 2var bar = obj.foo;
bar(); // -> 1
這時我就完全傻眼了,為啥兩個會不一樣,不是同一個函數嗎,哭哭喔。
這個疑問直到我看完 YDKJS 關於 this
的章節才比較有完整概念。以下是我看這篇的一些筆記。
所以我說那個 this 到底是什麼
這問題真的好,一旦研究下去才發現這問題比想像中複雜。我們會概略地分為四種規則,一一擊破。
- Default Binding
- Implicit Binding
- Explicit Binding
new
Binding
this
與呼叫該函數當下的位置 (誰呼叫該函數,在哪呼叫,如何呼叫,也就是 call-site) 有關,但與 lexical scope 無關。所以用區域變數的概念看待 this
常常會吃土。
Default Binding
先看一開始的範例。
function foo() {
console.log(this.a);
}var a = 1;foo(); // -> 1
像這種,直接呼叫某函數名稱 (reference),直白的呼叫,直接叫名字的直球對決,裡面的 this
就會是 global 。對了,如果你在 strict mode 使用 this
的話,會變成 undefined
。
Implicit Binding
看看一開始的第二個範例。
function foo() {
console.log(this.a);
}var a = 1;
var obj = {
a: 2,
foo: foo,
};obj.foo(); // -> 2
雖然我們可以直接像上一個那樣直白的使用 foo()
呼叫,但這次我們使用的隱晦呼叫,也就是用 obj.foo()
呼叫。像這種會需要用到點的,隱晦的表示他是在某個 Context 中,這時 this
就會指向這個 context ,就是點號前面的那串,也就是 obj
。
以下舉個例子,obj1 裡面有 obj2 裡面有 obj3 ,每個都有 property a 。最後使用 obj1.obj2.obj3.foo()
呼叫,究竟 this
會鹿死誰手呢。
function foo() {
console.log(this.a);
}var a = 0;
var obj1 = {
a: 1,
obj2: {
a: 2,
obj3: {
a: 3,
foo: foo
}
}
}obj1.obj2.obj3.foo(); // -> 3
這個呼叫隱含的 context 就是 obj1.obj2.obj3
,如果是這種點來點去的呼叫,就看最後一個點,他的左側全部就是黏起來,就是這次的 this
了。
Explicit Binding
這個說有多明顯真的可以很明顯。直接用 call
apply
bind
將 this
設定了。我們拿本篇前兩個範例來當例子。
function foo() {
console.log(this.a);
}var a = 1;
var obj = {
a: 2,
foo: foo,
};foo.call(obj) // -> 2
foo.apply(obj) // -> 2
foo.bind(obj)() // -> 2
用 call
或 apply
可以綁定目前的 context 並執行,而 bind 則是回傳一個綁好的函數。如果我們想要自己自幹一個陽春版的 bind
也可以。
function foo() {
console.log(this.a);
}function myBind(fn, obj) {
return function(...args) {
return fn.apply(obj, args)
}
}var a = 1;
var obj = {
a: 2,
};foo() // -> 1
var bar = myBind(foo, obj);
bar(); // -> 2
new
Binding
這牽扯到 new
跟 prototype
的機制,我盡量簡單帶過不要牽扯出更多鬼東西。在你 new
某個東西的時候他會幫你做下列事情。
- 建立空的物件
- 把空物件穿上 prototype (此處簡單帶過)
- 把空物件設為
this
,執行new
後面接的 function。 - 如果該 function 除非該 function 有回傳物件 (一般會盡量避免) ,否則會傳第三步被執行過的物件。
所以 new Binding 看起來是自成一派的怪東西。
誰先誰後
直接講結論。Default Binding 最弱不意外,如果你只是直白的呼叫一個函數,那直接設定 this
為 global 。事實上你有時候傳函數給 setTimeout ,他也會幫你用 Default Binding 執行,所以你常常會發現傳 callback 進去,結果他的 this
跟你想像的不一樣。
Explicit Binding 強於 Implicit Binding ,這也很直覺,畢竟都把 call 或是 apply 或是 bind 這種讓初學者恐懼的東西搬出來了。
雖然上面那個我們自幹的 myBind
則會強於 new Binding ,但最奇怪也最值得記錄的是如果使用內建 Bind 函數, new bind 會強於 Explicit Binding ,這跟內建 Bind 函數的實作有關。可以去看看 MDN 的 polyfill 學習更多。
function foo(something) {
this.a = something
}var obj1 = { a: 1 }
var b02 = foo.bind(obj1)var x02 = new b02(3)console.log(x02) // -> { a: 3}
console.log(obj1) // -> { a: 1}