關於 this
JavaScript 的 this 相信已經是一個討論到爛掉的話題了,但即便如此它仍然不是那麼好懂,為了加深印象這篇會用偏向筆記的形式記錄常見的 this 情境並加以說明。
其實 this 可以說簡單也可以說難,簡單呢因為它不過是 ECMAScript 規範裡的一部分,關於 this 指向誰背後都是有完整的定義,但我相信一般人沒事不會去讀那種鬼東西,難呢因為想在任何情況下都能迅速的答出 this 正確的值必須也要瞭解 JavaScript 的作用域以及各種能改變 this 的方法與情境,所以廢話不多說,我們開始吧。
this 指向誰
記住這句話"this 指向最後調用它的那個物件"就這麼簡單,讓我們看看以下的範例:
function func() {
console.log(this); // window
}
func(); // window.func();
在此調用 func
函數時其實是等價於 window.func
的因為:
非嚴格模式,瀏覽器的全域物件是
window
非嚴格模式,Node.js 的全域物件是
global
嚴格模式
'use strict';
下則會出現undefined
所以當我們調用 func
函數時其實只是省略了 window
而已,實際上它是被 window
所調用的,那麼鑒於上面所說的"this 指向最後調用它的那個物件",這個範例的 this
所指向的就是 window
。
var obj = {
func() {
console.log(this); // obj
},
};
obj.func();
相信這邊就很好懂了,最後調用 func
函數的物件是 obj
所以 this
自然是 obj
。
var obj = {
func() {
console.log(this); // obj
},
};
window.obj.func();
這邊在最後一行做了一點小改動,但答案依然是 obj
,仍然鑒於上面所說的那句話,最後調用函數的那個物件就是該函數的 this
,雖然最後一行的寫法很完整,但最後調用 func
函數的物件就是 obj
不是 window
,所以 this
就是指向 obj
。
var x = 'outside';
var obj = {
func() {
console.log(this.x); // undefined
},
};
window.obj.func();
這邊加入了一個全域變數 x
,然後我想在 func
函數裡取得 x
,但我得到的是 undefined
為什麼呢?一樣鑒於上面所說的那句話,調用 func
函數的是 obj
,意即 this
為 obj
,然而 obj
裡並沒有定義 x
所以 this.x
的值就是 undefined
。
var x = 'outside';
var obj = {
x: 'inside',
func() {
console.log(this.x); // outside
},
};
var func2 = window.obj.func;
func2(); // window.func2();
這邊你可能會有疑問,為什麼 this.x
不是 inside?一樣是那句話,我們可以先只看最後一行,調用 func2
函數實則等價於 window.func2
的調用,所以 func2
函數是被 window
所調用的,this
自然指向 window
,而 window.x
就是 outside ,所以說即便把 window.obj.func
賦值給了 func2
變數再做調用,this
的指向依然能驗證我們上面說的句話:"this 指向最後調用它的那個物件"。
var x = 'outside';
function func() {
var x = 'inside';
func2();
function func2() {
console.log(this.x);
}
}
func();
如果你有朗誦我們的金句 7749 遍加上理解以上的範例我想這題肯定是小菜一碟,就交給各位自行跑出答案囉~
第二種理解方式
JavaScript ES5 裡有三種函數調用方式:
func(v1, v2);
obj.func(v1, v2);
func.call(context, v1, v2);
一般在工作情境我們都是使用前兩者居多,但其實第三種調用方式,才是函數真正的調用形式。
func(v1, v2); // 等價於
func.call(undefined, v1, v2);
obj.func(v1, v2); // 等價於
obj.func.call(obj, v1, v2);
接下來我們就使用第三種調用形式來理解 this
到底是誰:
func.call(context, v1, v2);
this
就是第三種調用形式裡面的 context
,也就是第一個傳入的參數,如此而已,我們看看以下的範例:
function func() {
console.log(this);
}
func(); // func.call(undefined);
這段代碼在上面的小結出現過,一個基本的函數調用,在此我們用函數原生的 call
方法調用來理解最後一行的 func();
其實就等價於一旁的註解 func.call(undefined);
。
在 MDN 的 Function.prototype.call 裡面有提到:若此函數是在非嚴格模式下時,第一個參數為 null
、undefined
將會被置換成全域變數。在此我們的全域變數就是 window
。
var obj = {
func() {
console.log(this);
},
};
obj.func(); // obj.func.call(obj);
跟上一個範例一樣,我們在註解做了等價的代碼轉換示例,傳入的第一個參數 context
為 obj
所以 obj
就會成為該函數的 this
!
其實第二種理解方式跟第一種基本一樣,還是脫離不開"this 指向最後調用它的那個物件"這句話,只不過算是用另一個角度,透過函數原生的 call
方法來加深觀念罷了,以上只是為了理解用,相信你若聽過 call
大概率也知道 apply
與 bind
,多數情況會使用到它們都是為了改變 this
的指向,所以:
call、apply、bind
我們可以透過函數原生的 call
、apply
、bind
這三個方法來改變 this
的值,但這三個函數稍有不同,不過我們先來看看範例:
'use strict';
function func(v1, v2) {
console.log(this, v1, v2);
}
func('v1', 'v2'); // undefined "v1" "v2"
func.call(undefined, 'v1', 'v2'); // undefined "v1" "v2"
func.apply(undefined, ['v1', 'v2']); // undefined "v1" "v2"
這個範例就帶到上述重要的兩個觀念:
在嚴格模式下這樣呼叫
func('v1', 'v2');
的話this
即undefined
。第二種理解方式不斷提到的
func('v1', 'v2');
等價於func.call(undefined, 'v1', 'v2');
。
在這個範例都能被驗證,這樣對於 this
的思路是不是更清晰了呢~
那你可能會好奇 apply
呢?
apply
與 call
的差別只在於傳入的參數,call
的參數可以一個一個傳 apply
則只接受一個陣列,就這樣,除此之外基本一模一樣。
var x = 'window';
var obj = {
x: 'obj',
func(v1, v2) {
console.log(this.x, v1, v2);
},
};
var obj2 = {
x: 'obj2',
};
var func2 = obj.func.bind(obj2, 'v1', 'v2');
obj.func('v1', 'v2'); // obj v1 v2
func2(); // obj2 v1 v2
這邊示範了 bind
的用法,bind
與 call
、apply
的差別是它會回傳一個新的函數,而你傳入的參數就是該函數所綁定的 this
。
obj.func('v1', 'v2');
是普通的調用方式,答案也可想而知,但若我們想要調用該函數且希望 this
指向的是 obj2
這個物件,我們就使用 bind
做綁定,傳入的參數方式與 call
相同但我們需要將這個新函數賦值給一個變數再做調用。
以上就是可以改變 this
的三種方式,但要注意的一點,無論你使用 call
、apply
或 bind
,你傳入的 context
如果是基本型別都會被轉成物件:
function func() {
console.log(this);
}
func.call(1450); // Number {1450}
func.call('1450'); // String {'1450'}
在函數內部定義另一個變數存取 this
var obj = {
func1() {
console.log(this);
},
func2() {
setTimeout(function() {
console.log(this); // window
this.func1(); // Uncaught TypeError: this.func1 is not a function
}, 100);
},
};
obj.func2();
這個範例,如果我想在 setTimeout
裡調用 func1
函數該怎麼做?因為 setTimeout
裡的 this
是 window
然而 window
物件裡並沒有 func1
函數所以我們會得到錯誤訊息但我們可以這麼做:
var obj = {
func1() {
console.log(this);
},
func2() {
var that = this;
setTimeout(function() {
obj.func1(); // obj
that.func1(); // obj
}, 100);
},
};
obj.func2();
我們可以直接用 obj.func1();
的方式去調用也可以在 func2
函數裡的開頭就先定義另一個變數存取當前的 this
(obj
)後再做調用。
箭頭函數
理解箭頭函數的幾個重點:
箭頭函數的
this
指向的是該函數定義時的this
,並非執行時的。箭頭函數中沒有
this
,必須通過查找作用域來決定this
的值。
var obj = {
func1() {
console.log(this);
},
func2() {
console.log(this); // obj
setTimeout(() => {
this.func1();
});
},
};
obj.func2(); // obj
這個範例跟上面的一樣,但我不用定義另一個變數存取當前的 this
,用箭頭函數就可取得 obj
內的 func1
。
箭頭函數中沒有 this
,所以在 setTimeout
裡 this.func1();
調用時它會查找作用域取得 this
,也可以說在定義 this.func1();
時的 this
是什麼就是什麼,我們是在 func2
內定義 this.func1();
的 func2
內的 this
指向的是 obj
,所以 this.func1();
就等價於 obj.func1();
,執行的結果 this
就是 obj
。
總結
this
指向誰一開始真的很難搞懂,但仍然是前端開發者必須也是必懂的主題之一,至於為什麼這麼難懂呢?因為它是 JavaScript 啊 在大部分的物件導向程式語言裡 this
就是指向 instance
本身,所以並不難理解,但在 JavaScript 裡就真的猶如在迷宮般穿梭著,不過我相信光是這句"this 指向最後調用它的那個物件"就能解掉大部分關於 this
的題目了,至於有些更偏於規範而不常出現在實際工作情境的題目可能就得去查閱 ECMAScript 才知道了。