摘要:同步發布于是個程序員都知道函數,但是有些人不一定清楚函數式編程的概念。你的項目沒用到函數式編程,不代表項目不好。函數式編程的對立面就是命令式編程。函數式編程只是一個概念一致編碼方式,并沒有嚴格的定義。這里總結一些常用的函數式編程應用場景。
同步發布于 https://github.com/xianshanna...
是個程序員都知道函數,但是有些人不一定清楚函數式編程的概念。
應用的迭代使程序變得越來越復雜,那么程序員很有必要創造一個結構良好、可讀性好、重用性高和可維護性高的代碼。
函數式編程就是一個良好的代碼方式,但是這不代表函數式編程是必須的。你的項目沒用到函數式編程,不代表項目不好。
什么是函數式編程(FP)?函數式編程關心數據的映射,命令式編程關心解決問題的步驟。
函數式編程的對立面就是命令式編程。
函數式編程語言中的變量也不是命令式編程語言中的變量,即存儲狀態的單元,而是代數中的變量,即一個值的名稱。 變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個變量賦值。
函數式編程只是一個概念(一致編碼方式),并沒有嚴格的定義。本人根據網上的知識點,簡單的總結一下函數式編程的定義(本人總結,或許有人會不同意這個觀點)。
函數式編程就是純函數的應用,然后把不同的邏輯分離為許多獨立功能的純函數(模塊化思想),然后再整合在一起,變成復雜的功能。
什么是純函數?一個函數如果輸入確定,那么輸出結果是唯一確定的,并且沒有副作用,那么它就是純函數。
一般符合上面提到的兩點就算純函數:
相同的輸入必定產生相同的輸出
在計算的過程中,不會產生副作用
那怎么理解副作用呢?
簡單的說就是變量的值不可變,包括函數外部變量和函數內部變量。
所謂副作用,指的是函數內部與外部互動(最典型的情況,就是修改全局變量的值),產生運算以外的其他結果。
這里說明一下不可變,不可變指的是我們不能改變原來的變量值。或者原來變量值的改變,不能影響到返回結果。不是變量值本來就是不可變。
純函數特性對比例子上面的理論描述對于剛接觸這個概念的程序員,或許不好理解。下面會通過純函數的特點一一舉例說明。
輸入相同返回值相同純函數
function test(pi) { // 只要 pi 確定,返回結果就一定確定。 return pi + 2; } test(3);
非純函數
function test(pi) { // 隨機數返回值不確定 return pi + Math.random(); } test(3);返回值不受外部變量的影響
非純函數,返回值會被其他變量影響(說明有副作用),返回值不確定。
let a = 2; function test(pi) { // a 的值可能中途被修改 return pi + a; } a = 3; test(3);
非純函數,返回值受到對象 getter 的影響,返回結果不確定。
const obj = Object.create( {}, { bar: { get: function() { return Math.random(); }, }, } ); function test(obj) { // obj.a 的值是隨機數 return obj.a; } test(obj);
純函數,參數唯一,返回值確定。
function test(pi) { // 只要 pi 確定,返回結果就一定確定。 return pi + 2; } test(3);輸入值是不可以被改變的
非純函數,這個函數已經改變了外面 personInfo 的值了(產生了副作用)。
const personInfo = { firstName: "shannan", lastName: "xian" }; function revereName(p) { p.lastName = p.lastName .split("") .reverse() .join(""); p.firstName = p.firstName .split("") .reverse() .join(""); return `${p.firstName} ${p.lastName}`; } revereName(personInfo); console.log(personInfo); // 輸出 { firstName: "nannahs",lastName: "naix" } // personInfo 被修改了
純函數,這個函數不影響外部任意的變量。
const personInfo = { firstName: "shannan", lastName: "xian" }; function reverseName(p) { const lastName = p.lastName .split("") .reverse() .join(""); const firstName = p.firstName .split("") .reverse() .join(""); return `${firstName} ${lastName}`; } revereName(personInfo); console.log(personInfo); // 輸出 { firstName: "shannan",lastName: "xian" } // personInfo 還是原值
那么你們是不是有疑問,personInfo 對象是引用類型,異步操作的時候,中途改變了 personInfo,那么輸出結果那就可能不確定了。
如果函數存在異步操作,的確有存在這個問題,的確應該確保 personInfo 不能被外部再次改變(可以通過深度拷貝)。
但是,這個簡單的函數里面并沒有異步操作,reverseName 函數運行的那一刻 p 的值已經是確定的了,直到返回結果。
下面的異步操作才需要確保 personInfo 中途不會被改變:
async function reverseName(p) { await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = p.lastName .split("") .reverse() .join(""); const firstName = p.firstName .split("") .reverse() .join(""); return `${firstName} ${lastName}`; } const personInfo = { firstName: "shannan", lastName: "xian" }; async function run() { const newName = await reverseName(personInfo); console.log(newName); } run(); personInfo.firstName = "test"; // 輸出為 tset naix,因為異步操作的中途 firstName 被改變了
修改成下面的方式就可以確保 personInfo 中途的修改不影響異步操作:
// 這個才是純函數 async function reverseName(p) { // 淺層拷貝,這個對象并不復雜 const newP = { ...p }; await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = newP.lastName .split("") .reverse() .join(""); const firstName = newP.firstName .split("") .reverse() .join(""); return `${firstName} ${lastName}`; } const personInfo = { firstName: "shannan", lastName: "xian" }; // run 不是純函數 async function run() { const newName = await reverseName(personInfo); console.log(newName); } // 當然小先運行 run,然后再去改 personInfo 對象。 run(); personInfo.firstName = "test"; // 輸出為 nannahs naix
這個還是有個缺點,就是外部 personInfo 對象還是會被改到,但不影響之前已經運行的 run 函數。如果再次運行 run 函數,輸入都變了,輸出當然也變了。
參數和返回值可以是任意類型那么返回函數也是可以的。
function addX(y) { return function(x) { return x + y; }; }盡量只做一件事
當然這個要看實際應用場景,這里舉個簡單例子。
兩件事一起做(不太好的做法):
function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === "RE" && !task.completed) { filteredTasks.push({ ...task, userName: task.user.name }); } } return filteredTasks; } const filteredTasks = getFilteredTasks(tasks);
getFilteredTasks 也是純函數,但是下面的純函數更好。
兩件事分開做(推薦的做法):
function isPriorityTask(task) { return task.type === "RE" && !task.completed; } function toTaskView(task) { return { ...task, userName: task.user.name }; } let filteredTasks = tasks.filter(isPriorityTask).map(toTaskView);
isPriorityTask 和 toTaskView 就是純函數,而且都只做了一件事,也可以多帶帶反復使用。
結果可緩存根據純函數的定義,只要輸入確定,那么輸出結果就一定確定。我們就可以針對純函數返回結果進行緩存(緩存代理設計模式)。
const personInfo = { firstName: "shannan", lastName: "xian" }; function reverseName(firstName, lastName) { const newLastName = lastName .split("") .reverse() .join(""); const newFirstName = firstName .split("") .reverse() .join(""); console.log("在 proxyReverseName 中,相同的輸入,我只運行了一次"); return `${newFirstName} ${newLastName}`; } const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; }; })();函數式編程有什么優點?
實施函數式編程的思想,我們應該盡量讓我們的函數有以下的優點:
更容易理解
更容易重復使用
更容易測試
更容易維護
更容易重構
更容易優化
更容易推理
函數式編程有什么缺點?
性能可能相對來說較差
函數式編程可能會犧牲時間復雜度來換取了可讀性和維護性。但是呢,這個對用戶來說這個性能十分微小,有些場景甚至可忽略不計。前端一般場景不存在非常大的數據量計算,所以你盡可放心的使用函數式編程。看下上面提到個的例子(數據量要稍微大一點才好對比):
首先我們先賦值 10 萬條數據:
const tasks = []; for (let i = 0; i < 100000; i++) { tasks.push({ user: { name: "one", }, type: "RE", }); tasks.push({ user: { name: "two", }, type: "", }); }
兩件事一起做,代碼可讀性不夠好,理論上時間復雜度為 o(n),不考慮 push 的復雜度。
(function() { function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === "RE" && !task.completed) { filteredTasks.push({ ...task, userName: task.user.name }); } } return filteredTasks; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); getFilteredTasks(tasks); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(`第一種風格平均耗時:${averageTimeConsuming} 毫秒`); })();
兩件事分開做,代碼可讀性相對好,理論上時間復雜度接近 o(2n)
(function() { function isPriorityTask(task) { return task.type === "RE" && !task.completed; } function toTaskView(task) { return { ...task, userName: task.user.name }; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); tasks.filter(isPriorityTask).map(toTaskView); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(`第二種風格平均耗時:${averageTimeConsuming} 毫秒`); })();
上面的例子多次運行得出耗時平均值,在數據較少和較多的情況下,發現兩者平均值并沒有多大差別。10 萬條數據,運行 100 次取耗時平均值,第二種風格平均多耗時 15 毫秒左右,相當于 10 萬條數據多耗時 1.5 秒,1 萬條數多據耗時 150 毫秒(150 毫秒用戶基本感知不到)。
雖然理論上時間復雜度多了一倍,但是在數據不龐大的情況下(會有個臨界線的),這個性能相差其實并不大,完全可以犧牲瀏覽器用戶的這點性能換取可讀和可維護性。
很可能被過度使用
過度使用反而是項目維護性變差。有些人可能寫著寫著,就變成別人看不懂的代碼,自己覺得挺高大上的,但是你確定別人能快速的看懂不? 適當的使用才是合理的。
應用場景概念是概念,實際應用卻是五花八門,沒有實際應用,記住了也是死記硬背。這里總結一些常用的函數式編程應用場景。
簡單使用有時候很多人都用到了函數式的編程思想(最簡單的用法),但是沒有意識到而已。下面的列子就是最簡單的應用,這個不用怎么說明,根據上面的純函數特點,都應該看的明白。
function sum(a, b) { return a + b; }立即執行的匿名函數
匿名函數經常用于隔離內外部變量(變量不可變)。
const personInfo = { firstName: "shannan", lastName: "xian" }; function reverseName(firstName, lastName) { const newLastName = lastName .split("") .reverse() .join(""); const newFirstName = firstName .split("") .reverse() .join(""); console.log("在 proxyReverseName 中,相同的輸入,我只運行了一次"); return `${newFirstName} ${newLastName}`; } // 匿名函數 const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; }; })();JavaScript 的一些 API
如數組的 forEach、map、reduce、filter 等函數的思想就是函數式編程思想(返回新數組),我們并不需要使用 for 來處理。
const arr = [1, 2, "", false]; const newArr = arr.filter(Boolean); // 相當于 const newArr = arr.filter(value => Boolean(value))遞歸
遞歸也是一直常用的編程方式,可以代替 while 來處理一些邏輯,這樣的可讀性和上手度都比 while 簡單。
如下二叉樹所有節點求和例子:
const tree = { value: 0, left: { value: 1, left: { value: 3, }, }, right: { value: 2, right: { value: 4, }, }, };
while 的計算方式:
function sum(tree) { let sumValue = 0; // 使用列隊方式處理,使用棧也可以,處理順序不一樣 const stack = [tree]; while (stack.length !== 0) { const currentTree = stack.shift(); sumValue += currentTree.value; if (currentTree.left) { stack.push(currentTree.left); } if (currentTree.right) { stack.push(currentTree.right); } } return sumValue; }
遞歸的計算方式:
function sum(tree) { let sumValue = 0; if (tree && tree.value !== undefined) { sumValue += tree.value; if (tree.left) { sumValue += sum(tree.left); } if (tree.right) { sumValue += sum(tree.right); } } return sumValue; }
遞歸會比 while 代碼量少,而且可讀性更好,更容易理解。
鏈式編程如果接觸過 jquery,我們最熟悉的莫過于 jq 的鏈式便利了。現在 ES6 的數組操作也支持鏈式操作:
const arr = [1, 2, "", false]; const newArr = arr.filter(Boolean).map(String); // 輸出 "1", "2"]
或者我們自定義鏈式,加減乘除的鏈式運算:
function createOperation() { let theLastValue = 0; const plusTwoArguments = (a, b) => a + b; const multiplyTwoArguments = (a, b) => a * b; return { plus(...args) { theLastValue += args.reduce(plusTwoArguments); return this; }, subtract(...args) { theLastValue -= args.reduce(plusTwoArguments); return this; }, multiply(...args) { theLastValue *= args.reduce(multiplyTwoArguments); return this; }, divide(...args) { theLastValue /= args.reduce(multiplyTwoArguments); return this; }, valueOf() { const returnValue = theLastValue; // 獲取值的時候需要重置 theLastValue = 0; return returnValue; }, }; } const operaton = createOperation(); const result = operation .plus(1, 2, 3) .subtract(1, 3) .multiply(1, 2, 10) .divide(10, 5) .valueOf(); console.log(result);
當然上面的例子不完全都是函數式編程,因為 valueOf 的返回值就不確定。
高階函數高階函數(Higher Order Function),按照維基百科上面的定義,至少滿足下列一個條件的函數
函數作為參數傳入
返回值為一個函數
簡單的例子:
function add(a, b, fn) { return fn(a) + fn(b); } function fn(a) { return a * a; } add(2, 3, fn); // 13
還有一些我們平時常用高階的方法,如 map、reduce、filter、sort,以及現在常用的 redux 中的 connect 等高階組件也是高階函數。
柯里化(閉包)柯里化(Currying),又稱部分求值(Partial Evaluation),是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。
柯里化的作用以下優點:
參數復用
提前返回
延遲計算/運行
緩存計算值
柯里化實質就是閉包。其實上面的立即執行匿名函數的例子就用到了柯里化。
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2); // 3 // 柯里化之后 function addX(y) { return function(x) { return x + y; }; } addX(2)(1); // 3高階組件
這是組件化流行后的一個新概念,目前經常用到。ES6 語法中 class 只是個語法糖,實際上還是函數。
一個簡單例子:
class ComponentOne extends React.Component { render() { returntitle
; } } function HocComponent(Component) { Component.shouldComponentUpdate = function(nextProps, nextState) { if (this.props.id === nextProps.id) { return false; } return true; }; return Component; } export default HocComponent(ComponentOne);
深入理解高階組件請看這里。
無參數風格(Point-free)其實上面的一些例子已經使用了無參數風格。無參數風格不是沒參數,只是省略了多余參數的那一步。看下面的一些例子就很容易理解了。
范例一:
const arr = [1, 2, "", false]; const newArr = arr.filter(Boolean).map(String); // 有參數的用法如下: // arr.filter(value => Boolean(value)).map(value => String(value));
范例二:
const tasks = []; for (let i = 0; i < 1000; i++) { tasks.push({ user: { name: "one", }, type: "RE", }); tasks.push({ user: { name: "two", }, type: "", }); } function isPriorityTask(task) { return task.type === "RE" && !task.completed; } function toTaskView(task) { return { ...task, userName: task.user.name }; } tasks.filter(isPriorityTask).map(toTaskView);
范例三:
// 比如,現成的函數如下: var toUpperCase = function(str) { return str.toUpperCase(); }; var split = function(str) { return str.split(""); }; var reverse = function(arr) { return arr.reverse(); }; var join = function(arr) { return arr.join(""); }; // 現要由現成的函數定義一個 point-free 函數toUpperCaseAndReverse var toUpperCaseAndReverse = _.flowRight( join, reverse, split, toUpperCase ); // 自右向左流動執行 // toUpperCaseAndReverse是一個point-free函數,它定義時并無可識別參數。只是在其子函數中操縱參數。flowRight 是引入了 lodash 庫的組合函數,相當于 compose 組合函數 console.log(toUpperCaseAndReverse("abcd")); // => DCBA無參數風格優點?
參風格的好處就是不需要費心思去給它的參數進行命名,把一些現成的函數按需組合起來使用。更容易理解、代碼簡小,同時分離的回調函數,是可以復用的。如果使用了原生 js 如數組,還可以利用 Boolean 等構造函數的便捷性進行一些過濾操作。
無參數風格缺點?缺點就是需要熟悉無參數風格,剛接觸不可能就可以用得得心應手的。對于一些新手,可能第一時間理解起來沒那沒快。
參考文章Learn the fundamentals of functional programming?—?for free, in your inbox
Make your code easier to read with Functional Programming
從高階函數--->高階組件
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103272.html
摘要:為了盡可能提升互通性,已經成為函數式編程庫遵循的實際標準。與輕量級函數式編程的概念相反,它以火力全開的姿態進軍的函數式編程世界。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結,...
摘要:可當我們進行函數式編程時,這樣的方式會遇到困難,難點在于如何停止。而在函數式編程中,數據在管道中流動,上一個函數的返回值會傳給下一個函數,除非報錯,事先寫好的流程是停不下來的。 以下代碼會用到函數組合函數compose,只要知道compose是干什么的就足夠了,如果好奇具體的實現,可以看《JavaScript函數式編程之函數組合函數compose和pipe的實現》 在寫命令式的代碼時,...
摘要:所支持的面向對象編程包括原型繼承。發明于年的就是首批支持函數式編程的語言之一,而演算則可以說是孕育了這門語言。即使在今天,這個家族的編程語言應用范圍依然很廣。 1. 能說出來兩種對于 JavaScript 工程師很重要的編程范式么? JavaScript 是一門多范式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程...
摘要:所支持的面向對象編程包括原型繼承。發明于年的就是首批支持函數式編程的語言之一,而演算則可以說是孕育了這門語言。即使在今天,這個家族的編程語言應用范圍依然很廣。 1. 能說出來兩種對于 JavaScript 工程師很重要的編程范式么? JavaScript 是一門多范式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程...
閱讀 1837·2021-09-23 11:21
閱讀 698·2019-08-30 15:55
閱讀 832·2019-08-29 15:40
閱讀 528·2019-08-29 12:56
閱讀 3158·2019-08-26 12:00
閱讀 3552·2019-08-23 18:24
閱讀 2246·2019-08-23 17:08
閱讀 1637·2019-08-23 17:03