開發本身的前端工具庫(二):函數式編程

前言

本系列文章將經過本身的一個開發工具庫的實戰經驗(踩過的坑)教你們如何開發屬於本身的一個工具庫,在這裏你能夠學到Git的使用規範,基礎項目的搭建,代碼編寫的規範,函數式編程思想,TypeScript實戰,單元測試,編寫文檔和發佈NPM包等等知識。git

閱讀文章你可能須要如下基礎知識:es6

項目源碼

Windlike-Utilsgithub

系列目錄

  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)

柯里化就是傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。 咱們上面實現了一個加一的函數,但當咱們又須要一個加二的函數,又從新去寫代碼實現它的話效率是很低的,因此咱們就須要柯里化,咱們設想一下可不能夠這樣呢:app

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

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

這樣咱們就能夠很容易地獲得想要的函數,下面是add函數的實現:dom

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隨機數
複製代碼

總結

本章節講了函數式編程的一些主要概念,以及爲什麼用它來開發一個工具庫是很好的,由於純函數都是「乾淨」的,不依賴外部也不會對外部有影響,不用擔憂會影響到原有的代碼。

下章節咱們來說下如何爲本身的項目編寫測試用例。

相關文章
相關標籤/搜索