開發自己的前端工具庫(二):函數語言程式設計

後排的風過發表於2018-08-03

前言

本系列文章將通過自己的一個開發工具庫的實戰經驗(踩過的坑)教大家如何開發屬於自己的一個工具庫,在這裡你可以學到Git的使用規範,基礎專案的搭建,程式碼編寫的規範,函數語言程式設計思想,TypeScript實戰,單元測試,編寫文件和釋出NPM包等等知識。

閱讀文章你可能需要以下基礎知識:

專案原始碼

Windlike-Utils

系列目錄

  1. 開發自己的工具庫(一):專案搭建

為什麼要用函數語言程式設計

因為函數語言程式設計不會改變外部的變數,且對固定輸入有唯一輸出,這樣我們可以不管函式內部的具體實現去使用它,而且可以很方便地通過組合多個函式而成我們想要的那個函式,更接近自然語言的表達。

比如我們要實現一個y=f(x)=2*x+1的函式,通常我們會這麼寫:

function f(x) {
    return 2*x + 1;
}

f(1);  // 3
複製程式碼

而函數語言程式設計則是將他們拆分為幾個小函式,再組裝起來使用:

function double(x) {
    return 2*x;
}

function plusOne(x) {
    return x + 1;
}

plusOne(double(1));  // 3

// 或者還有更好一點的寫法,這裡暫未實現,
// 這裡只是寫下他們的呼叫方法,具體下面的文會講到
const doubleThenPlusOne = compose(plusOne, double);
doubleThenPlusOne(1);
複製程式碼

純函式

開發自己的前端工具庫(二):函數語言程式設計

  • 不可變性(immutable) 即對輸入的實參及外部變數不能進行改變,沒有副作用,以保證函式是“乾淨”的。
  • 唯一性 對每個固定輸入的引數,都有唯一對應的輸出結果,有點類似於數學裡的y=f(x),當輸入的x不變,輸出的y也不會改變

這是一個栗子:

const array = [1, 9, 9, 6];

// slice是純函式,因為它不會改變原陣列,且對固定的輸入有唯一的輸出
array.slice(1, 2);  // [9, 9]
array.slice(1, 2);  // [9, 9]

// splice不是純函式,它即改變原陣列,且對固定輸入,輸出的結果也不同
array.splice(0, 1);  // [9 ,9 ,6]
array.splice(0, 1);  // [9 ,6]
複製程式碼

柯里化(Currying)

柯里化就是傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。 我們上面實現了一個加一的函式,但當我們又需要一個加二的函式,又重新去寫程式碼實現它的話效率是很低的,所以我們就需要柯里化,我們設想一下可不可以這樣呢:

const plusOne = add(1);
const plusTwo = add(2);

plusOne(1);  // 2
plusTwo(2);  // 4
複製程式碼

這樣我們就可以很容易地得到想要的函式,下面是add函式的實現:

function add(a) {
    return function(b) {
        return a + b;
    }
}
複製程式碼

雖然基本滿足我們現在的需求,但感覺還是不太方便,如果我們要實現三個或多個數的相加我們可能得這樣寫:

function add(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}
複製程式碼

於是我們再設想一種更方便的方法:

function add(a, b, c) {
    return a + b + c;
}

const curryAdd = curry(add);
const plusOne = curryAdd(1);
const plusOneAndTwo = curryAdd(1, 2);

plusOne(2, 3);  // 6
plusOneAndTwo(3);  // 6
curryAdd(1)(2, 3);  // 6
curryAdd(1)(2)(3);  // 6
複製程式碼

這樣我們就可以自由產生需要引數不同的函式啦,下面是curry的實現方法(有興趣的同學可以先思考下再看):

  function curry<Return>(fn: Function): CurryFunction<Return> {
    // 記錄傳進來的函式總共需要多少個引數
    let paramsLength: number = fn.length;

    function closure(params: any[]): CurryFunction<Return> {

      let wrapper: CurryFunction<Return> = function (...newParams: any[]) {
        // 將所有的引數取出
        let allParams = [...params, ...newParams];

        if (allParams.length < paramsLength) {
          // 如果引數數量還不夠則返回新的函式
          return closure(allParams);
        } else {
          // 否則返回結果
          return fn.apply(null, allParams);
        }
      };

      return wrapper;
    }

    return closure([]);
  }
複製程式碼

可能有些不太好理解,一時看不懂的同學可以先跳過這裡看下面~

這裡是原始碼,及標頭檔案定義

另外也可以用原生的bind函式來實現柯里化:

const plusOne = add.bind(null, 1);

plusOne(2, 3);
複製程式碼

函式組合(Compose)

函式組合就是把多個不同的函式組合成一個新的函式。

比如這樣:

// 將函式從右往左組合
const doubleThenPlusOne = compose(plusOne, double);

// 1*2 + 1
doubleThenPlusOne(1);  // 3
複製程式碼
  function compose<Return>(...fn: any[]): (...params: any[]) => Return {
    return (...params: any[]): Return => {
      let i = fn.length - 1;
      let result = fn[i].apply(null, params);

      while (--i >= 0) {
        result = fn[i](result);
      }

      return result;
    };
  }
複製程式碼

這裡是原始碼,及標頭檔案定義

延遲輸出

有時候這個世界並不是那麼美好的,並不是所有的程式碼都是那麼“乾淨”的,比如I/O操作和DOM操作這些等待,因為這些操作都對外部有依賴,會對外部有影響。這時候就需要用延遲輸出來保證我們的函式是“乾淨”的,例如下面實現的這個random函式:

  function random(min: number = 0, max: number, float: boolean): () => number {
    return (): number => {
      if (min > max) {
        [min, max] = [max, min];
      }
      if (float || min % 1 || max % 1) {
        return min + Math.random() * (max - min);
      }

      return min + Math.floor(Math.random() * (max - min + 1));
    };
  }
複製程式碼

對於固定的輸入,它總返回的是產生符合條件的隨機數的函式,這樣我們就通過“拖延症”來讓我們的程式碼保持“乾淨”啦,是不是很機智呢!這樣做的好處還有它通過閉包機制把引數都記住,快取起來,下次可以不用重複傳同樣的引數:

const createRandomNumber = random(1, 100, false);

createRandomNumber();
createRandomNumber();  // 可以多次重複呼叫產生1到100隨機數
複製程式碼

總結

本章節講了函數語言程式設計的一些主要概念,以及為何用它來開發一個工具庫是很好的,因為純函式都是“乾淨”的,不依賴外部也不會對外部有影響,不用擔心會影響到原有的程式碼。

下章節我們來講下如何為自己的專案編寫測試用例。

相關文章