在編寫JavaScript應用的時候,我們經常會使用this
關鍵字。那麼this
關鍵字究竟是怎樣工作的?它的設計有哪些好的地方,有哪些不好的地方?本文帶大家全面系統地認識這個老朋友。
小明正在跑步,他看起來很開心
這裡的小明是主語,如果沒有這個主語,那麼後面的代詞『他』將毫無意義。有了主語,代詞才有了可以指代的事物。
類比到JavaScript的世界中,我們在呼叫一個物件的方法的時候,需要先指明這個物件,再指明要呼叫的方法。
1 2 3 4 5 6 7 8 |
var xiaoming = { name: 'Xiao Ming', run: function() { console.log(`${this.name} seems happy`); }, }; xiaoming.run(); |
在上面的例子中,第8行中的xiaoming
指定了run
方法執行時的主語。因此,在run
中,我們才可以用this
來代替xiaoming
這個物件。可以看到this
起了代詞的作用。
同樣的,對於一個JavaScript類,在將它初始化之後,我們也可以用類似的方法來理解:類的例項在呼叫其方法的時候,將作為主語,其方法中的this
就自然變成了指代主語的代詞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class People { constructor(name) { // 在用new關鍵字例項化一個物件的時候,相當於在說, // “建立一個People類例項(主語),它(this)的name是……” // 所以這裡的this就是新建立的People類例項 this.name = name; } run() { console.log(`${this.name} seems happy.`) } } // new關鍵字例項化一個類 var xiaoming = new People('xiaoming'); xiaoming.run(); |
這就是我認為this關鍵字設計得精彩的地方!如果將呼叫方法的語句(上面程式碼的第16行)和方法本身的程式碼連起來,像英語一樣讀,其實是完全通順的。
this
的繫結
句子的主語是可以變的,例如在下面的場景中,run
被賦值到小芳(xiaofang
)身上之後,呼叫xiaofang.run
,主語就變成了小芳!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var xiaofang = { name: 'Xiao Fang', }; var xiaoming = { name: 'Xiao Ming', run: function() { console.log(`${this.name} seems happy`); }, }; xiaofang.run = xiaoming.run; // 主語變成了小芳 xiaofang.run(); |
在這種情況下,句子還是通順的。所以,非常完美!
但是如果小明很摳門,不願意將run
方法借給小芳以後,this
就變成了小芳的話,那麼小明要怎麼做呢?他可以通過Function.prototype.bind讓run
執行時候的this
永遠為小明自己。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var xiaofang = { name: 'Xiao Fang', }; var xiaoming = { name: 'Xiao Ming', run: function() { console.log(`${this.name} seems happy`); }, }; // 將小明的run方法繫結(bind)後,返回的還是一個 // 函式,但是這個函式之後被呼叫的時候就算主語不是小明, // 它的this依然是小明 xiaoming.run = xiaoming.run.bind(xiaoming); xiaofang.run = xiaoming.run; // 主語雖然是小芳,但是最後this還是小明 xiaofang.run(); |
那麼同一個函式被多次bind
之後,到底this
是哪一次bind
的物件呢?你可以自己嘗試看看。
call
與apply
Function.prototype.call允許你在呼叫一個函式的時候指定它的this
的值。
1 2 3 4 5 6 7 8 9 10 11 |
var xiaoming = { name: 'Xiao Ming' }; function run(today, mood) { console.log(`Today is ${today}, ${this.name} seems ${mood}`); } // 函式的call方法第一個引數是this的值 // 後續只需按函式引數的順序傳參即可 run.call(xiaoming, 'Monday', 'happy') |
Function.prototype.apply和Function.prototype.call
的功能是一模一樣的,區別進在於,apply
裡將函式呼叫所需的所有引數放到一個陣列當中。
1 2 3 4 5 6 7 8 9 10 11 12 |
var xiaoming = { name: 'Xiao Ming' }; function run(today, mood) { console.log(`Today is ${today}, ${this.name} seems ${mood}`); } // apply只接受兩個引數 // 第二個引數是一個陣列,這個陣列的元素被按順序 // 作為run呼叫的引數 run.apply(xiaoming, ['Monday', 'happy']) |
那麼call
/apply
和上面的bind
混用的時候是什麼樣的行為呢?這個也留給大家自行驗證。但是在一般情況下,我們應該避免混用它們,否則會造成程式碼檢查或者除錯的時候難以跟蹤this
的值的問題。
當方法失去主語的時候,this
不再有?
其實大家可以發現我的用詞,當一個function
被呼叫的時候是有主語的時候,它是一個方法;當一個function
被呼叫的時候是沒有主語的時候,它是一個函式。當一個函式執行的時候,它雖然沒有主語,但是它的this
的值會是全域性物件。在瀏覽器裡,那就是window
。當然了,前提是函式沒有被bind
過,也不是被apply
或call
所呼叫。
那麼function
作為函式的情景有哪些呢?
首先,全域性函式的呼叫就是最簡單的一種。
1 2 3 4 |
function bar() { console.log(this === window); // 輸出:true } bar(); |
立即呼叫的函式表示式(IIFE,Immediately-Invoked Function Expression)也是沒有主語的,所以它被呼叫的時候this
也是全域性物件。
1 2 3 |
(function() { console.log(this === window); // 輸出:true })(); |
但是,當函式被執行在嚴格模式(strict-mode)下的時候,函式的呼叫時的this就是undefined
了。這是很值得注意的一點。
1 2 3 4 5 |
function bar() { 'use strict'; console.log('Case 2 ' + String(this === undefined)); // 輸出:undefined } bar(); |
不可見的呼叫
有時候,你沒有辦法看到你定義的函式是怎麼被呼叫的。因此,你就沒有辦法知道它的主語。下面是一個用jQuery新增事件監聽器的例子。
1 2 3 4 5 6 7 8 9 10 11 12 |
window.val = 'window val'; var obj = { val: 'obj val', foo: function() { $('#text').bind('click', function() { console.log(this.val); }); } }; obj.foo(); |
在事件的回撥函式(第6行開始定義的匿名函式)裡面,this
的值既不是window
,又不是obj
,而是頁面上id
為text
的HTML元素。
1 2 3 4 5 6 7 8 9 |
var obj = { foo: function() { $('#text').bind('click', function() { console.log(this === document.getElementById('text')); // 輸出:true }); } }; obj.foo(); |
這是因為匿名函式是被jQuery內部呼叫的,我們不知道它呼叫的時候的主語是什麼,或者是否被bind
等函式修改過this
的值。所以,當你將匿名函式交給程式的其他部分呼叫的時候,需要格外地謹慎。
如果我們想要在上面的回撥函式裡面使用obj的val
值,除了直接寫obj.val
之外,還可以在foo方法中用一個新的變數that
來儲存foo
執行時this
的值。這樣說有些繞口,我們看下例子便知。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
window.val = 'window val'; var obj = { val: 'obj val', foo: function() { var that = this; // 儲存this的引用到that,這裡的this實際上就是obj $('#text').bind('click', function() { console.log(that.val); // 輸出:obj val }); } }; obj.foo(); |
另外一種方法就是為該匿名函式bind
了。
1 2 3 4 5 6 7 8 9 10 11 12 |
window.val = 'window val'; var obj = { val: 'obj val', foo: function() { $('#text').bind('click', function() { console.log(this.val); // 輸出:obj val }.bind(this)); } }; obj.foo(); |
總結
在JavaScript中this
的用法的確是千奇百怪,但是如果利用自然語言的方式來理解,一切就順理成章了。不知道你讀完這篇文章時候理解了嗎?還是睡著了?親……醒醒……