前言
本系列文章將通過自己的一個開發工具庫的實戰經驗(踩過的坑)教大家如何開發屬於自己的一個工具庫,在這裡你可以學到Git的使用規範,基礎專案的搭建,程式碼編寫的規範,函數語言程式設計思想,TypeScript實戰,單元測試,編寫文件和釋出NPM包等等知識。
閱讀文章你可能需要以下基礎知識:
- Git的基本使用
- TypeScript
- ES6
專案原始碼
系列目錄
為什麼要用函數語言程式設計
因為函數語言程式設計不會改變外部的變數,且對固定輸入有唯一輸出,這樣我們可以不管函式內部的具體實現去使用它,而且可以很方便地通過組合多個函式而成我們想要的那個函式,更接近自然語言的表達。
比如我們要實現一個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隨機數
複製程式碼
總結
本章節講了函數語言程式設計的一些主要概念,以及為何用它來開發一個工具庫是很好的,因為純函式都是“乾淨”的,不依賴外部也不會對外部有影響,不用擔心會影響到原有的程式碼。
下章節我們來講下如何為自己的專案編寫測試用例。