0%

This 是如何運作的

記得初學 JavaScript 的時候,曾經和 42 的同學討論過學程式遇到的一些「抽象」的名詞概念,因為抽象所以難懂,我總是想要以類比的方式來找出一個合理且容易記住的解釋。這位同學本科是唸哲學(發現哲學轉職工程師的還真不少),他的第一個實習就找到了法國頗有名的電商公司。我們討論著 this 但是似乎怎麼解釋也無法清楚的描繪出來,只聽他重複著說著 this 就是 this…。

有些事真的需要「時間」來幫助我們理解,就像有些新認識的朋友,真的沒有相處過不會知道對方的個性,this 也是,寫 code 一陣子或許就會慢慢的、更清晰的去描繪 this 是怎麼回事。

「this」就像英文原意,是個代名詞,但更貼切的說 this 比較像是地方(空間)代名詞,以程式的面向來說也可以說是「物件」的代名詞,或是「執行環境」、「創建環境」或是「呼叫環境」?到底是哪一個,就要看用的是傳統或箭頭函式的寫法,或是…「呼叫」的方式?

我們常會說 this 到底是「指向」誰?那個誰應該就是包圍 this 的那個環境吧?我試著想像,這好像是我們說身體上的「指頭」,如果說手的指頭,那就是手臂;如果說是腳的指頭,就表示腿,這樣說當然只是一個比喻,但小鳥腦的我只能先這樣記著。

實際要理解 this 好像還是用程式會比較清楚,所以我們就以程式的方式來理解,也好險有時程式不用猜,把 this 用console.log()印出來,是最清楚的。當然,試著瞭解其背後運作原理與規則,也會幫助我們少踩一些雷吧!

自從 ES6 的箭頭函式出現, this 的指向就又有了其他可能,原因是使用箭頭函式時, this 的指向是異於傳統函式寫法的指向,這部分我們留到下次再來討論。

傳統函式的 this 指向

先瞭解一下,一個函式本身,可包含的預設參數有哪些?

1
2
3
4
5
6
var a = '全域宇宙';
function fn(params) {
console.log(params, this, window, arguments);
debugger;
}
fn(); // undefined, window {}, window{},arguments[]
  • 使用debugger會在執行中停止,這時在 DevTool 裡可看到ScopeScope表示函式的作用域。

以下這些參數是在運行函式時,本身就會存取的參數內容:

  • arguments會把此函式的「參數」以陣列裝成一個「類陣列」。
  • window 瀏覽器本身,屬全域變數(Global)。
  • this指向全域變數。

This 指向誰?

this在上述的函式裡是指向 window,但是在使用時,它可能會有許多不同的指向可能。指向會影響到框架的使用,例如希望this指向某個元件(物件),但卻指向 window,就會發生不預期的錯誤。

1
2
3
4
5
6
7
var obj = {
name: 'Tracy',
fn: function fn(params) {
console.log(params, this, window, arguments);
},
};
obj.fn(1, 2, 3); // undefined, obj{}, window{},arguments[1,2,3]

params, window, arguments 的結果和上述結果是一樣的,但是this卻不同,this指向了哪裡?
這次this指向本身的物件,所以我們在 console 可以看到物件裡的屬性和方法。

複雜的 This 指向

傳統函式寫法的this只與調用方式有關,以下使用var來定義變數(全域),如果使用letconst則有不同的結果(undefined)。

使用 Simple call 的方式呼叫:

1
2
3
4
5
6
var someone = '全域宇宙';
function callSomeone() {
console.log(this.someone);
}
// Simple call
callSomeone(); // 全域宇宙 <-- 指向全域

this 運用的變化

兩個同樣的函式callSomeone(),一個放在全域,一個用物件包起來,來測試指向。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var someone = '全域宇宙';
function callSomeone() {
console.log(this.someone);
}
callSomeone(); // 1. 全域宇宙'

// 2.在物件裡的 this 指向
var obj = {
someone: '在物件裡',
callSomeone() {
console.log(this.someone);
},
};
obj.callSomeone(); // 2. 在物件裡

// 3. 觀察指向
var obj2 = {
someone: '在第二個物件裡',
callSomeone,
};
obj2.callSomeone(); // 3. 在第二個物件裡

// 4. 試試它們的 this 指向誰?
var wrapObj = {
someone: '外層的物件',
callSomeone,
innerObj: {
someone: '內層的物件',
callSomeone,
},
};
wrapObj.callSomeone(); // 4. 外層的物件
wrapObj.innerObj.callSomeone(); // 4. 內層的物件

// 5. 用不同的方式呼叫,取 This
var obj3 = {
someone: '物件3',
fn() {
callSomeone(); // 全域宇宙 通常不太會這樣取this
},
};
obj3.fn();

// 6. 頗常見的調用 this 的方法
var obj4 = {
someone: '物件4',
fn() {
// callback function
setTimeout(function () {
console.log(this.someone);
}, timeout);
},
};
obj4.fn(); // 全域宇宙
  1. 如果函式本身在全域環境裡,呼叫時自然是指向全域
  2. 如果在呼叫這個函式時,是呼叫物件裡的函式,那麼this就會指向包裹它的物件obj.
  3. 如果在物件裡呼全域外的函式,裡面的變數也會先指向物件obj2本身。
  4. 如果物件有多層,那麼在呼叫的時候還是會指向上「ㄧ」層。
  5. 如果是在物件裡的函式,並且直接呼叫(使用 Simple call)的方式呼叫,這時this仍會指回向全域的物件變數(window),而不會指向本身的物件。
    通常不會使用 Simple call 的方式去調用this,因為this指向會不是我們想要的結果。而會使用在物件下的方式調用this
  6. callback function 大部分是屬於Simple call的形式,它的this大多指向全域,只有少部分的callback function會重新定義,this指向也會不同。

待續…

概念

調用函式的方式不同, this 的指向也會不同。

1. simple call

指在調用時,直接以函式名後加雙括號的方式調用,如sayHi()

2. Object methods

函式本身是包含在一個物件裡,也就是是物件的方法,如果要調用則以物件的方式調用,如obj.sayHi()

3. 建構式 new 創建的函式

以建構式 new 創建的函式,它的this指向,就會指向 new 這個函式的建構式的物件本身。

4.bind, call, append