ホームページ > 記事 > ウェブフロントエンド > JavaScript の関数型プログラミングとは何ですか?関数型プログラミングの概要
この記事の内容は、JavaScript における関数型プログラミングとは何ですか?関数型プログラミングの入門書は一定の参考価値があるので、困っている友人が参考になれば幸いです。
すべてのプログラマは function を知っていますが、関数プログラミングの概念を必ずしも理解していない人もいるかもしれません。
アプリケーションを繰り返すとプログラムはますます複雑になるため、プログラマは、適切に構造化され、読みやすく、再利用可能で、保守しやすいコードを作成する必要があります。
関数型プログラミングは優れたコーディング方法ですが、関数型プログラミングが必要であるという意味ではありません。プロジェクトで関数型プログラミングが使用されていないからといって、そのプロジェクトが悪いというわけではありません。
関数型プログラミング (FP) とは何ですか?
関数型プログラミングではデータのマッピングが考慮されますが、命令型プログラミングでは問題を解決するための手順が考慮されます。
関数型プログラミングの反対は、命令型プログラミングです。
関数型プログラミング言語の変数は 命令型プログラミング言語の変数、つまり状態を保存する単位ではなく、代数の変数です、つまり a 値の名前。変数の値は不変です。つまり、命令型プログラミング言語のように、変数への複数の代入は許可されません。
機能的プログラミングは単なる概念 (一貫したコーディング方法) であり、厳密な定義はありません。インターネット上の知識ポイントに基づいて、関数型プログラミングの定義を簡単にまとめます (私の要約です。この見解に同意しない人もいるかもしれません)。
関数型プログラミングは純粋関数の応用であり、さまざまなロジックを独立した関数を持つ多くの純粋関数に分割し(モジュール思考)、それらを統合して複雑な関数を作成します。
純粋関数とは何ですか?
関数の入力が決定され、出力結果が一意に決定され、副作用がない場合、それは純粋な関数です。
一般に、上記の 2 つの点を満たしている場合、それは純粋関数です。
同じ入力は同じ出力を生成する必要があります
計算の過程で、副作用はありません
それでは、副作用をどのように理解すればよいでしょうか?
簡単に言うと、関数の外部の変数と関数の内部の変数を含め、変数の値は不変です。
いわゆる 副作用とは、関数の内部と外部の間の相互作用を指します (最も一般的なケースは、グローバル変数の値の変更です)。これにより、他の影響が生じます。運用以外の結果。
ここで不変性について説明します 不変性とは、元の変数の値を変更できないことを意味します。または、元の変数値を変更しても、返される結果に影響を与えることはできません。変数値が本質的に不変であるというわけではありません。
純粋な関数の機能の比較例
上記の理論的な説明は、この概念に慣れていないプログラマにとっては理解しにくいかもしれません。以下に、純粋関数の特徴を例を挙げて 1 つずつ説明します。
#入力も戻り値も同じです
純粋な関数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);
関数が正しくありません。戻り値はオブジェクトゲッターの影響を受けており、戻り結果は不確かです。
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 が外部から (おそらくディープ コピーを通じて) 再度変更できないようにする必要があります。
ただし、この単純な関数には非同期操作はありません。p の値は、reverseName 関数が実行された時点で、結果が返されるまですでに決定されています。
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 関数を再度実行すると、入力が変更され、当然出力も変更されます。
パラメータと戻り値は任意の型にすることができますそれから、return 関数も可能です。
function addX(y) { return function(x) { return x + y; }; }1 つのことだけを実行してみてください
もちろん、これは実際のアプリケーションのシナリオによって異なりますが、ここでは簡単な例を示します。
2 つのことを一緒に行います (良い習慣ではありません):
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; }, pide(...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) .pide(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() { return <h1>title</h1>; } } 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 等构造函数的便捷性进行一些过滤操作。
无参数风格缺点?
缺点就是需要熟悉无参数风格,刚接触不可能就可以用得得心应手的。对于一些新手,可能第一时间理解起来没那没快。
以上がJavaScript の関数型プログラミングとは何ですか?関数型プログラミングの概要の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。