What's THIS in JavaScript ? [中]
延續上個主題,上回我們提到了 this
是誰,是取決於 function 被呼叫的方式,這次我們繼續來談談 this
與 function
的關係,以及 function 中的 .call()
、 .apply()
與 .bind()
是如何對 this
來進行操作的。
前情提要
這系列的主題其實是節錄自去年 (2016) 我在五倍紅寶石開設的課程,講的是 「This」 在 JavaScript 這門程式語言裡所代表的各種面貌。 然而最近無論是社群還是課堂教學,發現仍有不少剛入門的朋友對 JavaScript 的 This
代表的意義不太熟悉,那麼我想整理出這幾篇文章也許可以釐清你對 This
的誤解,反正資料也都還在,不如就整理出來與大家分享順便做個紀錄。
系列文快速連結:
"THIS" or "THAT" ?
相信大部分朋友第一次接觸 this
的時候,應該都是在處理「事件」(event) 的綁定吧?
當我們要取得觸發事件的元素時,如 click
, 這時候我們就會透過 this
來取得:
HTML
<button id="btn">按我</button>
JavaScript
var el = document.getElementById("btn");
el.addEventListener("click", function(event) {
console.log( this.textContent );
}, false);
像上面這樣,我們就可以透過 this.textContent
來取得觸發 click
事件的 <button>
裡面的文字。
雖然我們可以在執行事件的 callback function 裡面透過 this
來取得觸發事件的元素,但偶爾也會遇到像這樣的例子:
// ajax function
var $ajax = function(url, callback) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
if (typeof(callback) === 'function') {
callback.call(null, data);
}
}
};
request.send();
};
// button & click event.
var el = document.getElementById("btn");
// 按下按鈕後執行 ajax,但在 callback function 的 this 卻不是你想像中的那樣
el.addEventListener("click", function(event) {
console.log( this.textContent );
$ajax('[URL]', function(res) {
// this.textContent => undefined
console.log(this.textContent, res);
});
}, false);
像這種時候,最簡單的解法就是用另一個變數來對 this
做參考,像這樣:
el.addEventListener("click", function(event) {
// 透過 that 參考
var that = this;
console.log( this.textContent );
$ajax('[URL]', function(res) {
// this.textContent => undefined
console.log(that.textContent, res);
});
}, false);
上面這個例子透過 that
這個變數來指向原本的 this
,於是就可以在 callback function 裡的 scope 取得原本的 this.textContent
了。
強制指定 this 的方式
透過另一個變數來暫存 this
的方式雖然方便,那麼有沒有其他方式可以取得原本 this
的內容呢?
首先延續上一個範例,我們先看 bind()
。
在上個範例中,我們用 that
這個變數來替代 this
,以便取得觸發 click
事件的 el
:
el.addEventListener("click", function(event) {
// 透過 that 參考
var that = this;
console.log( this.textContent );
$ajax('[URL]', function(res) {
// this.textContent => undefined
console.log(that.textContent, res);
});
}, false);
如果用 bind()
改寫的話,可以像這樣:
el.addEventListener("click", function(event) {
console.log( this.textContent );
// 透過 bind(this) 來強制指定該 function scope 的 this
$ajax('[URL]', function(res) {
console.log(this.textContent, res);
}.bind(this));
}, false);
像上面這樣,在 function 後面加上 .bind(this)
就可以強制將 ( ) 內的物件帶入至 callback function 內,
於是 callback function 裡的 this
就會強制被指定成先前在 bind( )
裡面的內容了。
這裡有另一個簡單的比較:
var obj = {
x: 123
};
var func = function () {
console.log(this.x);
};
func(); // undefined
func.bind(obj)(); // 123
如果你從上篇就一路看到這裡的話,相信你一定知道為什麼 func()
的執行結果是 undefined
了。
那麼加上了 bind
之後的 func.bind(obj)()
執行的結果,會替我們將 func
的 this
暫時指向我們所設定的 obj
。
於是, console.log(this.x)
的結果自然就是 obj.x
也就是 123 了。
這裡你可以想像成把某個 function 在執行的時候,「暫時」把它掛在某個物件下,以便透過 this
去取得該物件的 Context。
實務上除了 ajax 的 callback function 以外,另外像是 setTimeout
、setInterval
這類的 function,也滿常見需要特別處理 this
的場景。
箭頭函數與 this
值得一提的是,從 ES6 開始新增了一種叫做 「箭頭函數表示式」 (Arrow Function expression) 的函數表達式, 而箭頭函數有兩個重要的特性:更短的函數寫法與 this 變數強制綁定。
像這樣,我們可以直接在 callback function 中取用 this.textContent
:
el.addEventListener("click", function(event) {
console.log( this.textContent );
// 箭頭函數隱含強制指定 this 至 callback function 中
$ajax('[URL]', res => {
console.log(this.textContent, res);
});
}, false);
但要注意的是,無論是使用 'use strict'
或是再加上 .bind(xxx)
都無法改變 this
的內容,也不能作為建構子 (constructor)來使用。
另外,也有一些場景適不適合用箭頭函數的,像是剛剛提到的事件綁定:
el.addEventListener("click", (event) => {
// 小心這裡的 this 變成了 「window」而不是 「el」!
console.log( this );
}, false);
或是像 VueJS 中的 methods
、 computed
會需要透過 this
來取得實體的情況等:
new Vue({
data: {
num: 0
},
methods: {
getNum_Wrong: () => {
// wrong!
return this.num;
},
getNum: function() {
// right!
return this.num;
}
}
});
箭頭函數方便歸方便,若是你的 function 內會有需要用到 this
的情況時,
就需要特別小心你的 this
是不是在不知不覺中換了人來當。
.call() 與 .apply()
既然講到了強制指定 this
的方式,看完了 bind()
、「箭頭函數」,接下來就不能不講到 call()
與 apply()
。
假設今天有個 function 長這樣:
function func( ){
// do something
}
那麼通常我們會直接這樣來呼叫它:
func( );
當然你也可以用 .call()
或是 .apply()
來呼叫它:
func.call( );
func.apply( );
你可能會覺得奇怪,看起來沒什麼不同對吧,還要多打幾個字豈不是自找麻煩。 但如果遇上了需要帶參數的時候,就又顯得有些不同。
基本上 .call()
或是 .apply()
都是去執行這個 function ,並將這個 function 的 context 替換成第一個參數帶入的物件,換句話說,就是強制指定某個物件作為該 function 的 this。
而 .call()
與 .apply()
的作用完全一樣,差別只在傳入參數的方式有所不同:
function func( arg1, arg2, ... ){
// do something
}
func.call( context, arg1, arg2, ... );
func.apply( context, [ arg1, arg2, ... ]);
.call()
傳入參數的方式是由「逗點」隔開,而 .apply()
則是傳入整個陣列作為參數。
var person = {
name: "Kuro",
hello: function(thing) {
console.log(this.name + " says hello " + thing);
}
}
var person2 = {
name: "Jack"
};
person.hello("world"); // "Kuro says hello world"
person.hello.call(person, "world"); // "Kuro says hello world"
person.hello.apply(person, ["world"]); // "Kuro says hello world"
person.hello.call(person2, "world"); // "Jack says hello world"
person.hello.apply(person2, ["world"]); // "Jack says hello world"
可以看到,同樣是呼叫 person.hello()
但是這裡可以透過 .call()
或是 .apply()
去指定當下的 this
是誰,
而差別只在傳入參數的方式有所不同。
bind, call, apply 的差異
bind()
讓這個 function 在呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 this
。
尤其常用在像是 callback function 這種類型的場景,可以想像成是先綁定 this
,然後讓 function 在需要時才被呼叫的類型。
而 .call()
與 .apply()
則是使用在 context 較常變動的場景,依照呼叫時的需要帶入不同的物件作為該 function 的 this
。
在呼叫的當下就立即執行。
在這次的篇幅中,我們簡單介紹了 .bind()
、.call()
、.apply()
以及「箭頭函數」是如何對 this
的操作,
那麼在下一篇文章當中,我們再來繼續探討「this 與前後文本 (context) 綁定的基本原則」,提供各位判斷「如何決定 this 是誰的順序」,
同時也透過例題與練習來讓各位驗證對於 JavaScript this
的了解。