在各種物件導向語言中,多半會有this之類的關鍵字存在,而JavaScript的this,大概是最受人矚目了吧!網路上永遠不缺下一篇文件來探討this,感覺就是個捉摸不定的對象,到底哪篇才是正確的解釋呢?

從底層的實作來看,這個this又會是怎樣的一個存在呢?

從函式的實作開始

因為函式可以隨意傳遞,亦可作為物件的方法操作,JavaScript的this是,動態地、依當時呼叫函式的方式而變化。

然而,對於來自嚴謹風格物件導向語言的開發者來說(例如Java),往往對此特性感到訝異,且覺得難以掌握,特別是在物件形成巢狀結構,或者是無法確定函式被呼叫的方式時(例如事件處理),經常無法辨別this為何物!

從語言的實作面來看,this在最單純的層面上,就只是個變數,實際參考值就視當時查找的環境物件而定。

而環境物件,就是先前專欄〈用抽象語法樹寫程式〉中談到的ctx,其中存放著變數名稱對應的值節點,例如,某變數節點variable代表'this',執行variable.evaluate(ctx)時,就是看ctx上有沒有'this'對應的值節點。

進一步地,若要瞭解this,必須先認識一下函式呼叫的實作。

假設stmts代表函式本體中全部陳述句節點的頂層節點,若函式定義有參數x,而呼叫函式時的引數為10,實際上就是將stmts作為new Assign(new Variable('x'), new Int(10))的子節點,如此在stmts.evaluate(ctx)時,就會在ctx上增加x與Int(10)節點的對應,因而stmts中若需要x,也就可以查找到對應的值節點。

而在這樣的實現方式下,如果我們要能在函式中使用this,就必須在某個時機點,執行new Assign(new Variable('this'), node),並將之作為函式本體stmts陳述的父節點,該時機點執行時的node是誰,函式之中的this,就會是誰。

apply與dot運算子

JavaScript本身提供了機制,可以直接指定this是誰,那就是函式物件的call或apply方法。

而這兩個方法的首個參數,都接受物件,作為this的實際對象,如果要自行實作apply方法,採用的概念如下,其中的args,會是實際上指定給函式的引數節點:

const target = targetParam.evaluate(ctx);
const args = argsParam.evaluate(ctx);
return new StmtSequence(
new Assign(new Variable('this'), target),
funcNode.bodyStmt(ctx, args)
).evaluate(ctx);

在JavaScript中,在無法確切掌握this是誰,或者必須固定this的對象時,往往會運用函式的apply或call明確指定。

像是:在瀏覽器環境中,經常會需要明確綁定this為某個DOM物件;比較難捉摸的是使用dot運算子時的this對象。

如先前專欄〈無拘的物件導向〉中談過的,dot運算子,可以看成是個二元運算子,而左運算元會是個實例,右運算元若是個函式呼叫,在取得函式本體陳述句節點之後,建立new Assign(new Variable('this'), leftOperand)作為函式本體陳述節點之父節點。

簡單來說,無論是apply或dot運算子,就是各自在呼叫函式之前,建立this作為隱含的參數,當函式不是透過這兩類方式呼叫時,理論上,就不會有隱含的this參數。

然而,在過去,JavaScript在不透過apply或dot直接呼叫函式時,this會直接參考至全域物件,著實耐人尋味。

JavaScript的this實作

在〈JavaScript的this原理〉(https://goo.gl/6HaKTP)中,也從JavaScript語言實作角度,探討了this的原理。

而根據作者所述,若想實現JavaScript風格的this,可以令this保存環境物件,相當於上述的ctx,而且this環境物件與實例在存取上,可設計為相同介面,這樣就無需區別兩者不同。

另一方面,就算在沒有透過apply或dot運算子呼叫函式之時,同樣也建立this作為函式呼叫時隱含的參數,並且令this指向全域環境物件,行為上就會與JavaScript類似了。

JavaScript令this本身就是環境物件的好處之一,應該是實現Closure時方便,當某個函式捕捉了外部函式的變數,並自函式呼叫結束後傳回,為了後續在呼叫被傳回的函式時,當中捕捉的變數仍然有效,必須有個方式令傳回的函式保留環境物件,而JavaScript的this扮演了這個角色。

如果不採取這個方式,就必須有額外的方式保存環境物件。

例如,我在ToyLang中的實現方式,是在函式本身保存環境物件,在呼叫函式時,若函式保存了環境物件,並且以該環境物件創造子環境物件,在函式中,就可以從子環境物件,一路往父環境物件查找被捕捉的變數。

而在這樣的作法之下,只有透過apply或者dot運算子,才會有this參數可用,this也就不會有指向全域的問題。

實際上,this指向全域在JavaScript中,也是個不好的特性,因此,在ECMAScript 5之後的嚴格模式(Strict mode)下,直接呼叫函式時,this也會是undefined了。

this是隱含的參數?

簡單來說,this本質上並不複雜,可視為函式呼叫時的參數之一,只不過在許多語言中,這個參數是隱含的。

然而,在Python裡面,倒是明確地要求,若函式會作為方法使用,第一個參數(通常命名為self)就必須接受被操作的實例,而這就是Python明確風格的展現。

當然,語言還是有其個別專屬的特性,例如,上述JavaScript風格的this實現,就有著兩種截然不同的方式。而這兩種方式,適用於不區分函式與方法差異的語言之中。

而在會區別函式與方法的語言中,方法中的this就還會有不同的實現方式,例如,從Python的實例取得方法的話,方法首個參數就必然綁定dot的左運算元,就算該方法被指定為另一實例作為特性,呼叫該方法時,也不會改變首個參數的值。

語言為了增加易用性,其實都會有不少隱含的行為,this只是其中一個例子,多數語言選擇把this參數當成是隱含的行為,Python卻是選擇明確,然而在某些地方它又具備有隱含的風格,而這正是不同語言各自擁有的獨特性。

除了從經驗與應用場合來掌握這類獨特性之外,如果能夠從實作層面來理解語言的這類機制,會是個更為務實的方式,像是在閱讀討論this這類的文件時,就更能理解其中探討的內容,或辨別內容的正確性,從而能夠以更為簡單的方式,來掌握這類變化多端的特性。

作者簡介


Advertisement

更多 iThome相關內容