這篇文章主要介紹了JavaScript中函數(shù)式編程指的是什么,具有一定借鑒價(jià)值,需要的朋友可以參考下。希望大家閱讀完這篇文章后大有收獲。下面讓小編帶著大家一起了解一下。
成都創(chuàng)新互聯(lián)專注于自流井企業(yè)網(wǎng)站建設(shè),成都響應(yīng)式網(wǎng)站建設(shè)公司,商城網(wǎng)站定制開發(fā)。自流井網(wǎng)站建設(shè)公司,為自流井等地區(qū)提供建站服務(wù)。全流程按需策劃,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)函數(shù)式編程的概念。
應(yīng)用的迭代使程序變得越來越復(fù)雜,那么程序員很有必要?jiǎng)?chuàng)造一個(gè)結(jié)構(gòu)良好、可讀性好、重用性高和可維護(hù)性高的代碼。
函數(shù)式編程就是一個(gè)良好的代碼方式,但是這不代表函數(shù)式編程是必須的。你的項(xiàng)目沒用到函數(shù)式編程,不代表項(xiàng)目不好。
什么是函數(shù)式編程(FP)?
函數(shù)式編程關(guān)心數(shù)據(jù)的映射,命令式編程關(guān)心解決問題的步驟。
函數(shù)式編程的對(duì)立面就是命令式編程。
函數(shù)式編程語言中的變量也不是命令式編程語言中的變量,即存儲(chǔ)狀態(tài)的單元,而是代數(shù)中的變量,即一個(gè)值的名稱。 變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個(gè)變量賦值。
函數(shù)式編程只是一個(gè)概念(一致編碼方式),并沒有嚴(yán)格的定義。本人根據(jù)網(wǎng)上的知識(shí)點(diǎn),簡(jiǎn)單的總結(jié)一下函數(shù)式編程的定義(本人總結(jié),或許有人會(huì)不同意這個(gè)觀點(diǎn))。
函數(shù)式編程就是純函數(shù)的應(yīng)用,然后把不同的邏輯分離為許多獨(dú)立功能的純函數(shù)(模塊化思想),然后再整合在一起,變成復(fù)雜的功能。
什么是純函數(shù)?
一個(gè)函數(shù)如果輸入確定,那么輸出結(jié)果是唯一確定的,并且沒有副作用,那么它就是純函數(shù)。
一般符合上面提到的兩點(diǎn)就算純函數(shù):
相同的輸入必定產(chǎn)生相同的輸出
在計(jì)算的過程中,不會(huì)產(chǎn)生副作用
那怎么理解副作用呢?
簡(jiǎn)單的說就是變量的值不可變,包括函數(shù)外部變量和函數(shù)內(nèi)部變量。
所謂副作用,指的是函數(shù)內(nèi)部與外部互動(dòng)(最典型的情況,就是修改全局變量的值),產(chǎn)生運(yùn)算以外的其他結(jié)果。
這里說明一下不可變,不可變指的是我們不能改變?cè)瓉淼淖兞恐??;蛘咴瓉碜兞恐档母淖儯荒苡绊懙椒祷亟Y(jié)果。不是變量值本來就是不可變。
純函數(shù)特性對(duì)比例子
上面的理論描述對(duì)于剛接觸這個(gè)概念的程序員,或許不好理解。下面會(huì)通過純函數(shù)的特點(diǎn)一一舉例說明。
輸入相同返回值相同
純函數(shù)
function test(pi) { // 只要 pi 確定,返回結(jié)果就一定確定。 return pi + 2; } test(3);
非純函數(shù)
function test(pi) { // 隨機(jī)數(shù)返回值不確定 return pi + Math.random(); } test(3);
返回值不受外部變量的影響
非純函數(shù),返回值會(huì)被其他變量影響(說明有副作用),返回值不確定。
let a = 2; function test(pi) { // a 的值可能中途被修改 return pi + a; } a = 3; test(3);
非純函數(shù),返回值受到對(duì)象 getter 的影響,返回結(jié)果不確定。
const obj = Object.create( {}, { bar: { get: function() { return Math.random(); }, }, } ); function test(obj) { // obj.a 的值是隨機(jī)數(shù) return obj.a; } test(obj);
純函數(shù),參數(shù)唯一,返回值確定。
function test(pi) { // 只要 pi 確定,返回結(jié)果就一定確定。 return pi + 2; } test(3);
輸入值是不可以被改變的
非純函數(shù),這個(gè)函數(shù)已經(jīng)改變了外面 personInfo 的值了(產(chǎn)生了副作用)。
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 被修改了
純函數(shù),這個(gè)函數(shù)不影響外部任意的變量。
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 對(duì)象是引用類型,異步操作的時(shí)候,中途改變了 personInfo,那么輸出結(jié)果那就可能不確定了。
如果函數(shù)存在異步操作,的確有存在這個(gè)問題,的確應(yīng)該確保 personInfo 不能被外部再次改變(可以通過深度拷貝)。
但是,這個(gè)簡(jiǎn)單的函數(shù)里面并沒有異步操作,reverseName 函數(shù)運(yùn)行的那一刻 p 的值已經(jīng)是確定的了,直到返回結(jié)果。
下面的異步操作才需要確保 personInfo 中途不會(huì)被改變:
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,因?yàn)楫惒讲僮鞯闹型?firstName 被改變了
修改成下面的方式就可以確保 personInfo 中途的修改不影響異步操作:
// 這個(gè)才是純函數(shù) async function reverseName(p) { // 淺層拷貝,這個(gè)對(duì)象并不復(fù)雜 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 不是純函數(shù) async function run() { const newName = await reverseName(personInfo); console.log(newName); } // 當(dāng)然小先運(yùn)行 run,然后再去改 personInfo 對(duì)象。 run(); personInfo.firstName = 'test'; // 輸出為 nannahs naix
這個(gè)還是有個(gè)缺點(diǎn),就是外部 personInfo 對(duì)象還是會(huì)被改到,但不影響之前已經(jīng)運(yùn)行的 run 函數(shù)。如果再次運(yùn)行 run 函數(shù),輸入都變了,輸出當(dāng)然也變了。
參數(shù)和返回值可以是任意類型
那么返回函數(shù)也是可以的。
function addX(y) { return function(x) { return x + y; }; }
盡量只做一件事
當(dāng)然這個(gè)要看實(shí)際應(yīng)用場(chǎng)景,這里舉個(gè)簡(jiǎn)單例子。
兩件事一起做(不太好的做法):
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 也是純函數(shù),但是下面的純函數(shù)更好。
兩件事分開做(推薦的做法):
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 就是純函數(shù),而且都只做了一件事,也可以單獨(dú)反復(fù)使用。
結(jié)果可緩存
根據(jù)純函數(shù)的定義,只要輸入確定,那么輸出結(jié)果就一定確定。我們就可以針對(duì)純函數(shù)返回結(jié)果進(jìn)行緩存(緩存代理設(shè)計(jì)模式)。
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 中,相同的輸入,我只運(yùn)行了一次'); 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]; }; })();
函數(shù)式編程有什么優(yōu)點(diǎn)?
實(shí)施函數(shù)式編程的思想,我們應(yīng)該盡量讓我們的函數(shù)有以下的優(yōu)點(diǎn):
更容易理解
更容易重復(fù)使用
更容易測(cè)試
更容易維護(hù)
更容易重構(gòu)
更容易優(yōu)化
更容易推理
函數(shù)式編程有什么缺點(diǎn)?
性能可能相對(duì)來說較差
函數(shù)式編程可能會(huì)犧牲時(shí)間復(fù)雜度來換取了可讀性和維護(hù)性。但是呢,這個(gè)對(duì)用戶來說這個(gè)性能十分微小,有些場(chǎng)景甚至可忽略不計(jì)。前端一般場(chǎng)景不存在非常大的數(shù)據(jù)量計(jì)算,所以你盡可放心的使用函數(shù)式編程??聪律厦嫣岬絺€(gè)的例子(數(shù)據(jù)量要稍微大一點(diǎn)才好對(duì)比):
首先我們先賦值 10 萬條數(shù)據(jù):
const tasks = []; for (let i = 0; i < 100000; i++) { tasks.push({ user: { name: 'one', }, type: 'RE', }); tasks.push({ user: { name: 'two', }, type: '', }); }
兩件事一起做,代碼可讀性不夠好,理論上時(shí)間復(fù)雜度為 o(n),不考慮 push 的復(fù)雜度。
(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(`第一種風(fēng)格平均耗時(shí):${averageTimeConsuming} 毫秒`); })();
兩件事分開做,代碼可讀性相對(duì)好,理論上時(shí)間復(fù)雜度接近 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(`第二種風(fēng)格平均耗時(shí):${averageTimeConsuming} 毫秒`); })();
上面的例子多次運(yùn)行得出耗時(shí)平均值,在數(shù)據(jù)較少和較多的情況下,發(fā)現(xiàn)兩者平均值并沒有多大差別。10 萬條數(shù)據(jù),運(yùn)行 100 次取耗時(shí)平均值,第二種風(fēng)格平均多耗時(shí) 15 毫秒左右,相當(dāng)于 10 萬條數(shù)據(jù)多耗時(shí) 1.5 秒,1 萬條數(shù)多據(jù)耗時(shí) 150 毫秒(150 毫秒用戶基本感知不到)。
雖然理論上時(shí)間復(fù)雜度多了一倍,但是在數(shù)據(jù)不龐大的情況下(會(huì)有個(gè)臨界線的),這個(gè)性能相差其實(shí)并不大,完全可以犧牲瀏覽器用戶的這點(diǎn)性能換取可讀和可維護(hù)性。
很可能被過度使用
過度使用反而是項(xiàng)目維護(hù)性變差。有些人可能寫著寫著,就變成別人看不懂的代碼,自己覺得挺高大上的,但是你確定別人能快速的看懂不? 適當(dāng)?shù)氖褂貌攀呛侠淼摹?/p>
應(yīng)用場(chǎng)景
概念是概念,實(shí)際應(yīng)用卻是五花八門,沒有實(shí)際應(yīng)用,記住了也是死記硬背。這里總結(jié)一些常用的函數(shù)式編程應(yīng)用場(chǎng)景。
簡(jiǎn)單使用
有時(shí)候很多人都用到了函數(shù)式的編程思想(最簡(jiǎn)單的用法),但是沒有意識(shí)到而已。下面的列子就是最簡(jiǎn)單的應(yīng)用,這個(gè)不用怎么說明,根據(jù)上面的純函數(shù)特點(diǎn),都應(yīng)該看的明白。
function sum(a, b) { return a + b; }
立即執(zhí)行的匿名函數(shù)
匿名函數(shù)經(jīng)常用于隔離內(nèi)外部變量(變量不可變)。
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 中,相同的輸入,我只運(yùn)行了一次'); return `${newFirstName} ${newLastName}`; } // 匿名函數(shù) 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
如數(shù)組的 forEach、map、reduce、filter 等函數(shù)的思想就是函數(shù)式編程思想(返回新數(shù)組),我們并不需要使用 for 來處理。
const arr = [1, 2, '', false]; const newArr = arr.filter(Boolean); // 相當(dāng)于 const newArr = arr.filter(value => Boolean(value))
遞歸
遞歸也是一直常用的編程方式,可以代替 while 來處理一些邏輯,這樣的可讀性和上手度都比 while 簡(jiǎn)單。
如下二叉樹所有節(jié)點(diǎn)求和例子:
const tree = { value: 0, left: { value: 1, left: { value: 3, }, }, right: { value: 2, right: { value: 4, }, }, };
while 的計(jì)算方式:
function sum(tree) { let sumValue = 0; // 使用列隊(duì)方式處理,使用棧也可以,處理順序不一樣 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; }
遞歸的計(jì)算方式:
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; }
遞歸會(huì)比 while 代碼量少,而且可讀性更好,更容易理解。
鏈?zhǔn)骄幊?/strong>
如果接觸過 jquery,我們最熟悉的莫過于 jq 的鏈?zhǔn)奖憷恕,F(xiàn)在 ES6 的數(shù)組操作也支持鏈?zhǔn)讲僮鳎?/p>
const arr = [1, 2, '', false]; const newArr = arr.filter(Boolean).map(String); // 輸出 "1", "2"]
或者我們自定義鏈?zhǔn)?,加減乘除的鏈?zhǔn)竭\(yùn)算:
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; }, pide(...args) { theLastValue /= args.reduce(multiplyTwoArguments); return this; }, valueOf() { const returnValue = theLastValue; // 獲取值的時(shí)候需要重置 theLastValue = 0; return returnValue; }, }; } const operaton = createOperation(); const result = operation .plus(1, 2, 3) .subtract(1, 3) .multiply(1, 2, 10) .pide(10, 5) .valueOf(); console.log(result);
當(dāng)然上面的例子不完全都是函數(shù)式編程,因?yàn)?valueOf 的返回值就不確定。
高階函數(shù)
高階函數(shù)(Higher Order Function),按照維基百科上面的定義,至少滿足下列一個(gè)條件的函數(shù)
函數(shù)作為參數(shù)傳入
返回值為一個(gè)函數(shù)
簡(jiǎn)單的例子:
function add(a, b, fn) { return fn(a) + fn(b); } function fn(a) { return a * a; } add(2, 3, fn); // 13
還有一些我們平時(shí)常用高階的方法,如 map、reduce、filter、sort,以及現(xiàn)在常用的 redux 中的 connect 等高階組件也是高階函數(shù)。
柯里化(閉包)
柯里化(Currying),又稱部分求值(Partial Evaluation),是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
柯里化的作用以下優(yōu)點(diǎn):
參數(shù)復(fù)用
提前返回
延遲計(jì)算/運(yùn)行
緩存計(jì)算值
柯里化實(shí)質(zhì)就是閉包。其實(shí)上面的立即執(zhí)行匿名函數(shù)的例子就用到了柯里化。
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2); // 3 // 柯里化之后 function addX(y) { return function(x) { return x + y; }; } addX(2)(1); // 3高階組件
這是組件化流行后的一個(gè)新概念,目前經(jīng)常用到。ES6 語法中 class 只是個(gè)語法糖,實(shí)際上還是函數(shù)。
一個(gè)簡(jiǎn)單例子:
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);
深入理解高階組件請(qǐng)看這里。
無參數(shù)風(fēng)格(Point-free)
其實(shí)上面的一些例子已經(jīng)使用了無參數(shù)風(fēng)格。無參數(shù)風(fēng)格不是沒參數(shù),只是省略了多余參數(shù)的那一步。看下面的一些例子就很容易理解了。
范例一:
const arr = [1, 2, '', false]; const newArr = arr.filter(Boolean).map(String); // 有參數(shù)的用法如下: // 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);
范例三:
// 比如,現(xiàn)成的函數(shù)如下: 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(''); }; // 現(xiàn)要由現(xiàn)成的函數(shù)定義一個(gè) point-free 函數(shù)toUpperCaseAndReverse var toUpperCaseAndReverse = _.flowRight( join, reverse, split, toUpperCase ); // 自右向左流動(dòng)執(zhí)行 // toUpperCaseAndReverse是一個(gè)point-free函數(shù),它定義時(shí)并無可識(shí)別參數(shù)。只是在其子函數(shù)中操縱參數(shù)。flowRight 是引入了 lodash 庫的組合函數(shù),相當(dāng)于 compose 組合函數(shù) console.log(toUpperCaseAndReverse('abcd')); // => DCBA
無參數(shù)風(fēng)格優(yōu)點(diǎn)?
參風(fēng)格的好處就是不需要費(fèi)心思去給它的參數(shù)進(jìn)行命名,把一些現(xiàn)成的函數(shù)按需組合起來使用。更容易理解、代碼簡(jiǎn)小,同時(shí)分離的回調(diào)函數(shù),是可以復(fù)用的。如果使用了原生 js 如數(shù)組,還可以利用 Boolean 等構(gòu)造函數(shù)的便捷性進(jìn)行一些過濾操作。
無參數(shù)風(fēng)格缺點(diǎn)?
缺點(diǎn)就是需要熟悉無參數(shù)風(fēng)格,剛接觸不可能就可以用得得心應(yīng)手的。對(duì)于一些新手,可能第一時(shí)間理解起來沒那沒快。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享JavaScript中函數(shù)式編程指的是什么內(nèi)容對(duì)大家有幫助,同時(shí)也希望大家多多支持創(chuàng)新互聯(lián)建站,關(guān)注創(chuàng)新互聯(lián)網(wǎng)站制作公司行業(yè)資訊頻道,遇到問題就找創(chuàng)新互聯(lián)建站,詳細(xì)的解決方法等著你來學(xué)習(xí)!