Side Effect Free Function

在 Functional Programming 裡面,有個名詞叫做 pure function,要稱為 pure function 要滿足兩個條件:

  1. 不管在什麼情況下,用什麼方法執行,相同的輸入參數一定會產生相同的輸出。
  2. 執行這個 function 不會產生任何副作用,副作用指的是像變數的污染等。

哪些 function 是 pure function 呢,簡單來說,大部分你所見過的數學函數都是 pure function,像是三角函數,sin、cos ,不管你什麼時候執行,用什麼方法執行,只要給同樣的輸入,輸出的結果就一定是一樣的,而且這些函數本身也不會對外部有任何的影響。

再來,function 執行會有什麼副作用呢?其實就是去存取其他外部的變數或函式時,改變了外部變數的數值,如果該變數有其他地方會使用,那可能會因為這些改變,造成程式的執行結果和預期的有出入,也就是產生了 bug,這其實也是為什麼會說要避免使用全域變數的原因。

要避免改變到外部變數其實還算簡單,除了做這事情本來就是目標之一的情形外(也就是你的 function 或是物件和其他東西會有相依性),程式在設計的時候有注意到應該都可以避免,那還有什麼情形可能造成意外的副作用呢?事實上,function 的執行方法的不同會有機會產生副作用:

var neko = {
    meow: function () {
        console.log(this);
    }
};

neko.meow(); // neko

var func = neko.meow;
func(); // window

上面的範例中,我定義了一個物件,並且給了它一個 function 作為 method,接著用兩種不同的方法來執行這個 method,然後會發現兩種執行方式會讓 function 內的 this 是不一樣的,而且很不巧的,this 在物件導向程式設計上,其實還蠻常會需要它的,因為你會需要存取該物件的屬性,最簡單的方法就是用 this 來代表該物件,設計上合理,語意上也合理,可是物件的 method 的執行方法的不同卻會讓它存取到不同的 this,結果就是會有意外的副作用,JavaScript 的這種特性其實也不全然是壞處,有種稱為 "borrowing method" (或是 code reuse)的 pattern 就可以利用這個特性,現在最可靠的判斷某變數是否是陣列的 方法,也是一種 "borrowing method" 的應用。

要確保 this 不變,有不少方法,像是多包一層用 applycall 來執行,或是用新的 bind 來指定好 this 的值,一些 JavaScript Library 也有對應的功能可以利用,像是 jQuery 的 proxy,不過用 apply 或是 call 來執行其實效率上會比較差,所以我想介紹的是另一種方法,利用 JavaScript 的另外一種特性:closure,closure 指的是,在多層的 variable scope 環境下,內層的 scope 可以去存取外層 scope 的變數,一個簡單例子:

var Dog = function () {
    var gender = 'male';

    this.getGender = function () {
        return gender;
    };
};

dog = new Dog();
dog.getGender(); // male

dog 這個物件會得到一個 method 叫做 getGender,而它會回傳在這個 method 外一層,也就是 dog 物件的建構函式裡面定義的 gender 變數,這個 method 並不會因為他的 scope 內沒有 gender 變數就噴出錯誤訊息,取而代之的,它會往上一層的 variable scope 找同樣名稱的變數,一直找到最外層,也就是 root 物件那層,以網頁應用的話,root 物件就是 window 了,如果還是找不到才有機會出現錯誤訊息,利用這個特性,就可以完全的避免使用到 this 這個關鍵字來建立物件,不使用 this 的話自然就沒有我上面說的副作用了,這樣該物件的 method 不管是怎樣執行的,都不會影響到內部去存取的變數。

jQuery 裡面也有使用到這種技巧, jQuery 的 Callbacks 就是這樣子設計的物件,所以你可以在使用 Callbacks 的 Deferred 裡面看到這樣的程式碼

deferred[ tuple[0] ] = list.fire;
deferred[ tuple[0] + "With" ] = list.fireWith;

前面的 tuple[0] 是 'resolve', 'reject' 或 'notify',而 list 就是相對應的 Callbacks 物件,這段程式碼實際上就是在定義 Deferred 物件的 resolve, resolveWith, reject, rejectWith... 等屬性,可以看到他的指派方法就是直接把 Callbacks 物件的 fire 和 fireWith method 借給 Deferred 物件,所以執行 Deferred.resolve 其實就等同於執行對應 Callbacks 物件的 fire 方法,而由於 Callback 內部沒有使用到 this ,所以這樣的使用完全是沒有問題的。

這樣子的用法有什麼好處呢?我們可以先反過來看看要確保 this 正確的話,程式碼要改成怎樣:

deferred[ tuple[0] ] = function () {
    list.fire();
};
deferred[ tuple[0] + "With" ] =  function () {
    list.fireWith();
};

這樣子寫可以確保 list 裡面方法的 this 就是 Callbacks 物件本身,不過其實這樣改會造成 jQuery 的 Chain Ability 爛掉,請不要真的去改。這樣的寫法有兩個缺點:

  1. 多了一層 scope,雖然現在瀏覽器的 JavaScript 引擎讓 scope 層數和 performance 之間的影響比以前小很多了,不過還是能少就少。
  2. 程式碼變得比較不漂亮,程式碼漂不漂亮和好不好讀、好不好維護息息相關。

Side effect free 的 function 還有哪些地方可以用呢,除了像 jQuery 這樣供物件之間呼叫執行,最多的還是作為 callback function 了吧,不管是事件的 callback function 還是 XHR 的 callback function,都可以利用到這些好處,讓程式碼更好看,也減少 scope chain 的層數。