Js中函数式编程

时间: 2020-04-11阅读: 134标签: 函数

最近和做技术的朋友聊天的时候,发现自己居然不能将函数式编程思想讲清楚,于是做一次复习

 

一、函数是“一等公民”

常常都能听到这么一句话:JavaScript 中,函数是“一等公民”,这句话到底意味着什么?

在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量 —— Christopher Strachey

其实在很多传统语言中( 比如 C,JAVA 8 以前 )函数只可以声明和调用,无法像字符串一样作为参数使用

而 JavaScript 中的函数与其他数据类型处于平等地位,这是函数式编程的前提

 

二、纯函数 (pure functions)

现在正式接触函数式编程,首先看一个简单的需求:

有这样的一堆用户信息

const arr = [
  {name: '赵信', gender: 1, age: 25, high: 176, weight: 62}, 
  {name: '艾希', gender: 2, age: 23, high: 161, weight: 46}, 
  {name: '阿狸', gender: 2, age: 27, high: 182, weight: 53}, 
  {name: '盖伦', gender: 1, age: 27, high: 175, weight: 78}, 
  {name: '沃里克', gender: 1, age: 42, high: 169, weight: 70}, 
  {name: '安妮', gender: 2, age: 16, high: 153, weight: 43}, 
  {name: '卡尔玛', gender: 2, age: 40, high: 168, weight: 48}, 
  {name: '菲兹', gender: 0, age: 52, high: 163, weight: 50}, 
  {name: '亚索', gender: 1, age: 35, high: 177, weight: 65}, 
  {name: '锐雯', gender: 2, age: 33, high: 172, weight: 52}, 
]

编写一个过滤用户信息的函数,统计18岁以上男性有多少人,且记录他们的身高和姓名

也许你会这么写:

const male = {
  count: 0,
  list: [],
};

const MIN_AGE = 18;

const Count = (arr) => {
  for (const item of arr) {
    if (
      !item 
      || +item.age < +MIN_AGE 
      || `${item.gender}` !== '1'
    ) { continue }
    male.count++;
    male.list.push({
      name: item.name,
      high: item.high,
    });
  }
}

似乎没什么问题的亚子,我们工作中也会写这样的函数

但上面的 MIN_AGE、male 都是外部变量(或者说全局变量)

我们在写业务的时候,这样的写法挑不出什么毛病,但他们都不是纯函数

纯函数具备两个特点:

1. 不依赖外部状态,相同的输入永远得到相同的输出;

2. 没有副作用,不会修改入参或者全局变量。 // splice 说的就是你!

就上面的例子来说,如果连续执行几次 Count(arr) 就会出问题:

如果按照纯函数的标准,可以改成这样:

const Count = (arr, min) => {
  // 创建一个局部变量
  const res = {
    count: 0,
    list: [],
  };
  for (const item of arr) {
    if (
      !item 
      || +item.age < +min // 使用入参而不是全局变量
      || `${item.gender}` !== '1'
    ) { continue }
    res.count++;
    res.list.push({
      name: item.name,
      high: item.high,
    });
  }
  // 返回结果
  return res;
}

这样调整之后,函数就实现了完全的自给自足,我们也能很清楚的知道这个函数所依赖的参数是什么

但仅仅是这样的调整似乎没有什么特别之处,假如我们筛选条件改为体重小于 50kg 的女性,这个函数就需要做许多调整

别急,我们才刚开始,接下来就打造一个易维护、可读性高的业务函数

 

三、柯里化 (curry)

上面的例子其实采用的是命令式编程的思想,关注的是如何一步一步实现当前的需求

函数式编程更像是用一个一个的加工站组合起来的工厂流水线,他也能实现需求,但更关注的是如何使用加工站

这个加工站就是柯里化,柯里化的概念很简单:将一个多参数函数,转换成一个依次调用的单参数函数

fun(a, b, c)  ->  fun(a)(b)(c)

需要注意柯里化和局部调用的区别

局部调用是指:只传递给函数一部分参数,并返回一个函数去处理剩下的参数

fun(a, b, c) -> fun(a)(b, c) / fun(a, b)(c)

不过在实际工作中,由于都是使用工具库(比如 Lodash,Ramda)提供的 curry 函数,而这些 curry 函数通常既满足柯里化,也满足局部调用,所以这两个概念对实际工作没什么影响

先从一个简单的例子来认识柯里化,首先声明一个求和函数

const sum = (x, y, z) => x + y + z;

然后实现一个简单的 curry 函数(通常我们不会自己去写 curry 函数,而是直接使用各种工具库提供的 curry 函数

const curry = (fn) => {
    return function recursive(...args) {
        // 如果args.length >= fn.length则表明传入了足够的参数,此时调用fn并返回
        if (args.length >= fn.length) {
            return fn(...args);
        }

        // 否则表明没有传入足够的参数,此时返回一个函数,用这个函数接受后面传递的新参数
        return (...newArgs) => {
            // 递归调用recursive函数,并返回
            return recursive(...args.concat(newArgs));
        };
    };
};

将 sum 函数柯里化

const Sum = curry(sum);      // -> [Function]
Sum(10)(11)(12); // -> 33
const Sum10 = Sum(10); // -> [Function] const Sum10_11 = Sum10(11); // -> [Function] Sum10_11(12); // -> 33

我们可以直接使用柯里化之后的 Sum 来得到最终结果,也可以基于 Sum 创建出两个特定的单入参函数 Sum10 和 Sum10_11,大大的增强了原本的 sum 函数的灵活性

而这些单入参函数是函数组合的基础。

 

四、函数组合 (compose) 

如果一个值要经过多个函数才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这就是函数组合

const compose = (f, g) => x => f(g(x))

以这个极简版的 compose 函数举个例子:

const f = x => x + 1;
const g = x => x * 2;
const fg = compose(f, g);
fg(1)  // ----> ?

别用控制台调试,能看出 fg(1) 的结果是 3 还是 4 么?

如果有经过思考,就会发现一个细节:函数组合中的函数是倒序执行的,我们的入参是 (f, g),但实际执行的顺序是 g -> f

现在假设我们有四个工具函数:

filter18(arr);            // 从数组中返回年龄大于18岁的数据
filterMale(arr);          // 从数组中筛选出男性数据并返回新数组
pickNameHeight(arr);      // 获取数组中的姓名和身高字段并返回新数组
log(arr); // 打印参数

按照命令式编程的思路,如果要通过这四个函数实现最初的那个筛选用户信息的需求,就需要这么写:

log(pickNameHeight(filterMale(filter18(arr))));

看得眼花是不是?使用 compose 试试:

const fun = compose(log, pickNameHeight, filterMale, filter18);
fun(arr);

现在就清晰多了,通过入参我们能一眼看出这条流水线做了什么

而且将不同的函数用不同的方式组合,还能得到更多更灵活的函数,这恰恰是函数式编程的魅力所在

和 curry 函数一样,我们通常都是直接使用各种工具库提供的 compose 函数

而这些工具库通常还会提供一个 pipe 函数,这个函数的作用 compose 类似,但 pipe 的执行顺序和 compose 相反,会将入参函数从前往后组合

现在我们掌握了函数式编程的两大利器: curry 和 compose,再回头想想最开始的那个需求吧

 

五、实战

再来过一遍需求:编写一个过滤用户信息的函数,统计18岁以上男性有多少人,且记录他们的身高和姓名

其实我们只需要做三件事,首先过滤出18岁以上的数据,然后过滤出男性,最后获取其身高和姓名

1. 过滤出18岁以上的数据,首先需要实现一个用于比较大小的工具函数

// 校验对象中的某个 key 是否大于临界值 val
function porpGt(key, val, item) {  
return item[key] > val }

将这个函数柯里化,就能得到过滤 18 岁的工具函数

const cPropGt = curry(porpGt);        // porpGt(a, b, c) -> cPropGt(a)(b)(c)
const filter18 = cPropGt('age')(18);  // cPropGt('age')(18)(item) -> filter18(item)
arr.filter(filter18);                 // 返回 age 大于 18 的数据

2. 过滤出男性,这需要一个判断等值的工具函数

// 判断对象中的某个 key 是否等于临界值 val
function porpEq(key, val, item) {
  return `${item[key]}` === `${val}`
}

同样的执行柯里化,然后得到过滤男性的工具函数

const cPropEq = curry(porpEq);            // porpEq(a, b, c) -> cPropEq(a)(b)(c)
const filterMale = cPropEq('gender')(1);  // cPropEq('gender')(1)(item) -> filterMale(item)
arr.filter(filterMale);                   // 返回 gender 等于 1 的数据

3. 记录身高和姓名,需要一个从对象中提取值的工具函数

// 从对象中提取多个值并返回新的对象
function pickAll(keys, item) {
  const res = {};
  keys.map(key => res[key] = item[key]);
  return res;
}

柯里化,并保留 name 和 high 两个字段

const cPickAll = curry(pickAll); 
const pickProps = cPickAll(['name', 'high']); 
arr.map(pickProps);   // 只保留 name 和 high

完成这三步之后,如果采用面向对象的写法,可以直接链式调用:

arr.filter(filter18)
  .filter(filterMale)
  .map(pickProps)

而如果使用了工具库,通常会带有 filter()、map() 这样的工具函数,其功能和数据的 filter、map 一样,只是调用的方式有些区别

所以使用工具库的话,就可以很方便的使用函数组合:

const Count = compose(
  map(pickProps),
  filter(filterMale),
  filter(filter18),
);
Count(arr);

如果需要调整过滤条件,就只需要稍微修改一下工具函数的入参,生成新的工具函数之后再组合即可

 

六、小结

函数式编程会让代码显得更清晰,更易维护

但从上面的例子也可以看出,命令式的写法只进行了一次遍历,而函数式编程的写法却遍历了三次

所以我想提醒看到这里的小伙伴,函数式编程并不是放之四海皆准的万能药, 甚至在某些性能要求很严格的场合,函数式编程并不是太合适的选择

我认为命令式编程、面向对象编程、函数式编程之间的关系就像是汽车、轮船、飞机之间的关系一样

他们之间并不存在绝对的优劣好坏,也许在大部分的场合,飞机的速度会比汽车更快,但在崇山峻岭之间,飞机也没法安然着陆

多学习一种编程思想,只是多掌握了一门技能,仅此而已。

原文:https://www.cnblogs.com/wisewrong/p/12531629.html

站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/8959

如何实现 lodash.get 函数及可选链操作简化取值

lodash 基本上成为了 js 项目的标配工具函数,广泛应用在各种服务端以及前端应用中,但是它的包体积略大了一些。对于服务端来说,包的体积并不是十分的重要,或者换句话说,不像前端那样对包的体积特别敏感,一分一毫都会影响页面打开的性能,从而影响用户体验。

Js中的普通函数与构造函数比较

想必学过javascript函数的同学想必能细心的发现,同样是函数,为什么有个函数要加上new关键字呢,加上他们的意义又是什么,作用于什么场景,下面我们就来给大家详细介绍一下。什么是构造函数?构造函数的优点与缺点?

ES6新特性之箭头函数与function的区别

写法不同;在function中,this指向的是调用该函数的对象;而在箭头函数中,this永远指向定义函数的环境。箭头函数不可以当构造函数;function存在变量提升,可以定义在调用语句后;箭头函数以字面量形式赋值,是不存在变量提升的;

浅析js中的纯函数、高阶函数、记忆函数、偏函数

上周分享文档中遇到几个关键名称,纯函数、高阶函数、记忆函数、偏函数....,这里做一下解析与举例,纯函数是函数式编程中非常重要的一个概念,简单来说,就是一个函数的返回结果只依赖于它的参数

ES6 Array.find()和findIndex()函数用法

ES6为Array增加了find(),findIndex函数。find()函数用来查找目标元素,找到就返回该元素,找不到返回undefined,而findIndex()函数也是查找目标元素,找到就返回元素的位置,找不到就返回-1。

javascript封装函数

使用函数有两步:1、定义函数,又叫声明函数, 封装函数。2、调用函数var 变量 = 函数名(实参);对函数的参数和返回值的理解

理解 JavaScript Mutation 突变和 PureFunction 纯函数

不可变性、纯函数、副作用,状态可变这些单词我们几乎每天都会见到,但我们几乎不知道他们是如何工作的,以及他们是什么,他们为软件开发带来了什么好处。在这篇文章中,我们将深入研究所有这些,以便真正了解它们是什么以及如何利用它们来提高我们的Web应用程序的性能。

Js高阶函数(Heigher-order function)

满足以下条件:接受一个或多个函数作为输入/输出一个函数;高阶函数一般是那些函数型包含多于函数。在函数式编程中,返回另一个函数的高阶函数被称为Curry化的函数。

工作中常用的 JavaScript 函数片段

查找数组最小,同上,不明白为什么要分成两个题目。Math.max 换成 Math.min,s>n?s:n 换成 s<n?s:n,(n,m)=>m-n 换成 (n,m)=>n-m,或者直接取最后一个元素

JS异常函数之箭头函数

在JS中,箭头函数可以像普通函数一样以多种方式使用。但是,它们一般用于需要匿名函数表达式,例如回调函数。下面示例显示举例箭头函数作为回调函数,尤其是对于map(), filter(), reduce(), sort()等数组方法。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广