JavaScript-可維護代碼編寫,函數式編程與純函數

JavaScript-可維護代碼編寫,函數式編程與純函數

JavaScript是函數式編程與面向對象編程的混合編程語言,加上本身一些可擴展性(比如:函數參數個數及類型的不確定),使得JavaScript非常靈活,當然也可以說非常不可控。正是這個特點,使得一個團隊維護一個共同的前端項目時,JavaScript代碼可能非常難以讀懂。試想,你新加入一個團隊,讓你去讀別人的代碼本身就不太容易,如果團隊的註釋習慣及編程風格不太好,真的很難讀懂每個js文件到底在幹什麼。因此,編寫可維護的高質量代碼真的非常重要

1. 問題來源

閱讀代碼比閱讀文章困難是因爲,一段代碼可能會依賴很多函數,你爲了瞭解每個函數的作用不得不跳到其他代碼塊,這不符合我們的閱讀習慣。 偶爾在一本書中看到這樣一句話:“面嚮對象語言的問題是,它們永遠都要隨身攜帶那些隱式的環境。你只需要一個香蕉,但卻得到一個拿着香蕉的大猩猩…以及整個叢林”。所以,嘗試在我們的代碼中更巧妙利用JavaScript的特點,使用函數式編程,可以使得我們的JS代碼可維護性更高,可讀性更強。

2. 使用純函數來提高JS代碼可維護性

首先我們來了解一下什麼是函數式編程。

在函數式編程語言中,函數是第一類的對象,也就是說,函數 不依賴於任何其他的對象而可以獨立存在,而在面向對象的語言中,函數 ( 方法 ) 是依附於對象的,屬於對象的一部分。這一點就 決定了函數在函數式語言中的一些特別的性質,比如作爲傳出 / 傳入參數,作爲一個普通的變量等。

我們常聽到這樣一種說法,在JavaScript中函數是“一等公民”。當我們說函數是“一等公民”的時候,我們實際上說的是它們和其他對象都一樣,你可以像對待任何其他數據類型一樣對待它們——把它們存在數組裏,當作參數傳遞,賦值給變量…等等。而函數式編程的一個基礎就是純函數

純函數是這樣一種函數,即相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用。

2.1 理解純函數

實際上我們可以這樣理解,純函數就是這樣一種函數,它不依賴於外部環境(例如:全局變量、DOM)、不改變外部環境(例如:發送請求、改變DOM結構),函數的輸出完全由函數的輸入決定。
比如 slice 和 splice,這兩個函數的作用並無二致——但是注意,它們各自的方式卻大不同,但不管怎麼說作用還是一樣的。我們說 slice 符合純函數的定義是因爲對相同的輸入它保證能返回相同的輸出。而 splice 卻會嚼爛調用它的那個數組,然後再吐出來;這就會產生可觀察到的副作用,即這個數組永久地改變了。

相關代碼請查看github

        var array1 = [0,1,2,3,4,5,6];
        var array2 = [0,1,2,3,4,5,6];

        var spliceArray = array1.splice(0,2);
        var sliceArray = array2.slice(0,2);

        console.log('array1: ' + array1);
        console.log('spliceArray: ' + spliceArray);

        console.log('array2: ' + array2);
        console.log('sliceArray: ' + sliceArray); 

運行結果

array1: 2,3,4,5,6
spliceArray: 0,1
array2: 0,1,2,3,4,5,6
sliceArray: 0,1

可以看到,splice改變了原始數組,而slice沒有。我們認爲,slice不改變原來數組的方式更加“安全”。改變原始組數,是一種“副作用”。

2.2 非純函數可能帶來的“副作用”

讓我們來仔細研究一下“副作用”以便加深理解。那麼,我們在純函數定義中提到的萬分邪惡的副作用到底是什麼?

副作用是在計算結果的過程中,系統狀態的一種變化,或者與外部世界進行的可觀察的交互。

副作用可能包含,但不限於:

  • 列表內容
  • 更改文件系統
  • 往數據庫插入記錄
  • 發送一個 http 請求
  • 可變數據
  • 打印/log
    獲取用戶輸入
  • DOM 查詢
  • 訪問系統狀態
    這個列表還可以繼續寫下去。概括來講,只要是跟函數外部環境發生的交互就都是副作用——這一點可能會讓你懷疑無副作用編程的可行性。函數式編程的哲學就是假定副作用是造成不正當行爲的主要原因。
        function getName(obj){
            return obj.name;
        }
        function getAge(obj){
            return obj.age;
        }
        function selfIntroduction(people){
            console.log(getName(people));
            console.log(getAge(people));
        }

        var Lee = {
            name: 'LYY',
            age: 25
        };

        selfIntroduction(Lee);

運行結果

LYY
25

顯然selfIntroduction這個函數不是純函數,它依賴於getNamegetAge兩個函數,如果我不小心改變了其中某個函數的功能,這將使得selfIntroduction這個函數出現錯誤。你現在可能感覺自己不會犯這樣的錯誤,但當網頁變得複雜,且由多人維護的時候,這將是個很難發現的bug。

2.3 純函數編程的優點

  1. 可緩存性(Cacheable)
    純函數總能夠根據輸入來做緩存。實現緩存的一種典型方式是 memoize 技術:
        var memoize = function(f) {
            var cache = {};

            return function() {
                var arg_str = JSON.stringify(arguments);
                cache[arg_str] = cache[arg_str] ? cache[arg_str] + '(from cache)' : f.apply(f, arguments);
                return cache[arg_str];
            };
        };

        var squareNumber  = memoize(function(x){ return x*x; });

        console.log(squareNumber(4));       
        console.log(squareNumber(4)); 
        console.log(squareNumber(5));
        console.log(squareNumber(5)); 

執行結果:

16
16(from cache)
25
25(from cache)
  1. 可移植(Portable)
    純函數是完全自給自足的,它需要的所有東西都能輕易獲得。仔細思考思考這一點…這種自給自足的好處是什麼呢?首先,純函數的依賴很明確,因此更易於觀察和理解——沒有偷偷摸摸的小動作。這使得你在閱讀這種代碼的時候更容易,一個函數完成一個功能,不再依賴其他函數或者變量。
  2. 可測試(Testable)
    第三點,純函數讓測試更加容易。因爲只要每次輸入相同,純函數將輸出相同的結果,不需要多次測試同一個輸入。
  3. 合理性(Reasonable)
    很多人相信使用純函數最大的好處是引用透明性(referential transparency)。如果一段代碼可以替換成它執行所得的結果,而且是在不改變整個程序行爲的前提下替換的,那麼我們就說這段代碼是引用透明的。
    由於純函數總是能夠根據相同的輸入返回相同的輸出,所以它們就能夠保證總是返回同一個結果,這也就保證了引用透明性。
  4. 並行代碼(Parallel)
    最後一點,也是決定性的一點:我們可以並行運行任意純函數。因爲純函數根本不需要訪問共享的內存,而且根據其定義,純函數也不會因副作用而進入競爭態(race condition)。
    並行代碼在服務端 js 環境以及使用了 web workers 的瀏覽器那裏是非常容易實現的,因爲它們使用了線程(thread)。不過出於對非純函數複雜度的考慮,當前主流觀點還是避免使用這種並行。

3. 不要濫用函數式編程或者純函數

以上分析提到了純函數的很多優點,但是,這並不是要求我們編寫的每一個函數都是純函數。函數越“純”,對環境依賴越少,往往意味着要輸入更多參數。

3.1 可以提純的情況舉例

var pureHttpCall = memoize(function(url, params){
  return function() { return $.getJSON(url, params); }
});

這裏有趣的地方在於我們並沒有真正發送 http 請求——只是返回了一個函數,當調用它的時候纔會發請求。這個函數之所以有資格成爲純函數,是因爲它總是會根據相同的輸入返回相同的輸出:給定了 url 和 params 之後,它就只會返回同一個發送 http 請求的函數。這種技巧結合 柯里化 及 代碼組合會使我們的JS代碼清晰、可維護。

柯里化 我寫過一篇博客,你可以點擊閱讀。 代碼組合其實比較簡單,現在舉例如下。

        var compose = function(f, g) {
            return function(x) {
                return f(g(x));
            };
        };

        function getPeople(){
            return {};
        }

        function namePeople(p){
            p.name = 'Lee';
            return p;
        }

        var definePeople = compose(namePeople, getPeople);
        var people = definePeople();
        console.log(people);

執行結果:
Object {name: “Lee”}
看,代碼組合就是字面意思。

3.2 濫用函數式編程的舉例

var getServerStuff = function(callback){
  return ajaxCall(function(json){
    return callback(json);
  });
};
//如果你仔細分析這段代碼,它就等價於
var getServerStuff = ajaxCall;

分析過程:

// 這行
return ajaxCall(function(json){
  return callback(json);
});

// 等價於這行
return ajaxCall(callback);

// 那麼,重構下 getServerStuff
var getServerStuff = function(callback){
  return ajaxCall(callback);
};

// ...就等於
var getServerStuff = ajaxCall;

還有這種控制器:

var BlogController = (function() {
  var index = function(posts) {
    return Views.index(posts);
  };

  var show = function(post) {
    return Views.show(post);
  };

  var create = function(attrs) {
    return Db.create(attrs);
  };

  var update = function(post, attrs) {
    return Db.update(post, attrs);
  };

  var destroy = function(post) {
    return Db.destroy(post);
  };

  return {index: index, show: show, create: create, update: update, destroy: destroy};
})();

我們可以直接把它重寫成:

var BlogController = {index: Views.index, show: Views.show, create: Db.create, update: Db.update, destroy: Db.destroy};

當你濫用函數式編程時,很可能使得你的代碼很難懂。所以,我的建議是,代碼保持最直接簡潔的狀態,儘量使用純函數(容易維護)、適當情況下使用函數式編程(容易看懂)

發佈了82 篇原創文章 · 獲贊 82 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章