JavaScript中的函數語言程式設計

蔣鵬飛發表於2020-02-09

函數語言程式設計

函數語言程式設計是一種程式設計正規化,是一種構建計算機程式結構和元素的風格,它把計算看作是對數學函式的評估,避免了狀態的變化和資料的可變,與函數語言程式設計相對的是指令式程式設計。我們有這樣一個需求,給陣列的每個數字加一:

// 陣列每個數字加一, 指令式程式設計
let arr = [1, 2, 3, 4];
let newArr = [];
for(let i = 0; i < arr.length; i++){
  newArr.push(arr[i] + 1);
}

console.log(newArr); // [2, 3, 4, 5]

這段程式碼結果沒有問題,但是沒法重用。我們換一個思維,這裡麵包含的操作其實就兩個,一個是遍歷陣列,一個是成員加一。我們把這兩個方法拆出來:

// 先拆加一出來
let add1 = x => x +1;

// 然後拆遍歷方法出來,通過遍歷返回一個操作後的新陣列
// fn是我們需要對每個陣列想進行的操作
let createArr = (arr, fn) => {
  const newArr = [];
  for(let i = 0; i < arr.length; i++){
    newArr.push(fn(arr[i]));
  }

  return newArr;
} 

// 用這兩個方法來得到我們期望的結果
const arr = [1, 2, 3, 4];
const newArr = createArr(arr, add1);
console.log(newArr);  // [2, 3, 4, 5], 結果仍然是對的

這樣拆分後,如果我們下次的需求是對陣列每個元素乘以2,我們只需要寫一個乘法的方法,然後複用之前的程式碼就行:

let multiply2 = x => x * 2;

// 呼叫之前的createArr
const arr2 = [1, 2, 3, 4];
const newArr2 = createArr(arr2, multiply2);
console.log(newArr2);  // [2, 4, 6, 8], 結果是對的

事實上我們的加一函式只能加一,也不好複用,它還可以繼續拆:

// 先寫一個通用加法,他接收第一個加數,返回一個方法
// 返回的這個方法接收第二個加數,第一個加數是上層方法的a
// 這樣當我們需要計算1+2是,就是add(1)(2)
let add = (a) => {
  return (b) => {
    return a + b;
  }
}

// 我們也可以將返回的函式賦給一個變數,這個變數也就變成一個能特定加a的一個方法
let add1 = add(1);

let res = add1(4); 
console.log(res);  // 5

所以函數語言程式設計就是將程式分解為一些更可重用、更可靠且更易於理解的部分,然後將他們組合起來,形成一個更易推理的程式整體。

純函式

純函式是指一個函式,如果它的呼叫引數相同,則永遠返回相同的結果。它不依賴於程式執行期間函式外部任何狀態或資料的變化,只依賴於其輸入引數。同時函式的執行也不改變任何外部資料,它只通過它的返回值與外部通訊。

下面這個函式就不是純函式,因為函式內部需要的discount需要從外部獲取:

let discount = 0.8;
const calPrice = price => price * discount;
let price = calPrice(200);  // 160

// 當discount變了,calPrice傳同樣額引數,結果不一樣,所以不純
discount = 0.9;
price = calPrice(200);  // 180

要改為純函式也很簡單,將discount作為引數傳遞進去就行了

const calPrice = (price, discount) => price * discount;

純函式可以保證程式碼的穩定性,因為相同的輸入永遠會得到相同結果。不純的函式可能會帶來副作用。

函式副作用

函式副作用是指呼叫函式時除了返回函式值之外,還對主呼叫函式產生附加的影響,比如修改全域性變數或者外部變數,或者修改引數。這可能會帶來難以查詢的問題並降低程式碼的可讀性。下面的foo就有副作用,當後面有其他地方需要使用a,可能就會拿到一個被汙染的值

let a = 5;
let foo = () => a = a * 10;
foo();
console.log(a); // 50

除了我們自己寫的函式有副作用外,一些原生API也可能有副作用,我們寫程式碼時應該注意:

image-20200109232215022

我們的目標是儘可能的減少副作用,將函式寫為純函式,下面這個不純的函式使用了new Date,每次執行結果不一樣,是不純的:

image-20200109232541307

要給為純函式可以將依賴注入進去,所謂依賴注入就是將不純的部分提取出來作為引數,這樣我們可以讓副作用程式碼集中在外部,遠離核心程式碼,保證核心程式碼的穩定性

// 依賴注入
const foo = (d, log, something) => {
  const dt = d.toISOString();
  return log(`${dt}: ${something}`);
}

const something = 'log content';
const d = new Date();
const log = console.log.bind(console);
foo(d, log, something);

所以減少副作用一般的方法就是:

1. 函式使用引數進行運算,不要修改引數
2. 函式內部不修改外部變數
3. 運算結果通過返回值返回給外部

可變性和不可變性

  • 可變性:指一個變數建立以後可以任意修改
  • 不可變性: 指一個變數被建立後永遠不會發生改變,不可變性是函數語言程式設計的核心概念

下面是一個可變的例子:

image-20200109233313733

如果我們一定要修改這個引數,我們應該將這個引數進行深拷貝後再操作,這樣就不會修改引數了:

image-20200109233515929

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。

作者博文GitHub專案地址: https://github.com/dennis-jiang/Front-End-Knowledges

相關文章