问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

什么是函数式编程

发布网友 发布时间:2022-05-03 04:22

我来回答

2个回答

懂视网 时间:2022-04-22 14:38

函数式编程的对立面就是命令式编程。

函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。 变量的值是不可变的(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;
 },
 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 等构造函数的便捷性进行一些过滤操作。

无参数风格缺点?

缺点就是需要熟悉无参数风格,刚接触不可能就可以用得得心应手的。对于一些新手,可能第一时间理解起来没那没快。

热心网友 时间:2022-04-22 11:46

在维基百科中,已经对函数式编程有了很详细的介绍。
那我们就来摘取一下Wiki上对Functional Programming的定义:
In
computer science, functional programming is a programming paradigm that
treats computation as the evaluation of mathematical functions and
avoids state and mutable data.
简单地翻译一下,也就是说函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
为什么来大姨妈胸会胀 少儿学什么舞蹈 青年学什么舞蹈好 成年人学什么舞蹈 福州企业最低工资标准 2013年厦门的底薪是多少 生产要素的需求有哪些性质 生产要素的需求有何特点? 什么是生产要素需求 微观经济学要素需求什么是条件要素需求?它和要素需求有什么不同?_百度... C语言是函数式编程语言吗 javascript 是函数式编程语言吗 羊肉粉丝锅的做法,羊肉粉丝锅怎么做好吃,羊肉 江西瑞州汽运集团牛牛汽运有限公司怎么样? C语言都有哪些函数 函数式编程的特点 Java函数式编程语言是什么? 江西牛牛乳业有限责任公司怎么样? 哪些编程语言区别了函数function和过程procedure? 大家都学习或者使用过什么函数式的编程语言啊? 函数式编程语言有哪些 哪些编程语言是函数式的 一分钟要珍惜,写出感受50字 关于珍惜时间的名言警句感受 读书心得体会关于惜时的 读《珍惜时间,热爱学习》有感 鲁迅先生珍惜时间感受50字 例假来颜色黑的怎么回事来例假不能用太大力是那 读珍惜时间的感受30字 最近几个月例假不正常,基本是两个月来一次,而且颜色刚开始黑黑的,请问是什么原因,如何治疗,调理? 现在函数式编程用什么语言好? 支付宝天天领红包,谁都可以领吗,具体怎么做, 江西有哪些鲜奶品牌 在江西省高速违章在福建泉州德化县可以处理吗? 个性标签怎么弄qq上 怎样才能打败牛牛乐的竞争对手?牛牛乐是江西吉安的一种乳制品商标哦! QQ上个性标签咋弄? 江西都市网络信息技术有限公司怎么样? 手机QQ上个性标签怎么弄 江西宜春*原*颜赣辉被“双开”,涉及贪污数据到底有多大? 定远到江西三清山多少公里 江西IPTV(电信电视),网络机顶盒用的是维创E900-S 怎么投屏 女人么样B乃才是完美 2乃是什么意思 女人的乳汁是什么味道的 急翻译英语 我是女生,却爱上另一个女生,该怎么办? 能不能介绍几首好听的英文歌 英文介绍大笨钟 女人出轨有什么表现