JS函数式编程范式

1、什么是函数式编程

函数式编程的基本理念是只关注做什么而不是怎么做,即把函数提升到第一等的地位为核心来组织代码,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。

2、纯函数

上面的描述仅仅只是只是简单的概括了一下函数式编程,想要更加深入明了还需要明白一些概念:纯函数、高阶函数、函数的柯里化、组合函数。

首先说一下什么叫纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。举以下两两个例子就明白了

// 非纯函数,函数的输入与输出的对应并不是固定的,会受到外面常量NUM的影响
const NUM = 2;
const getResult = n => NUM * n;
// 纯函数,输入与输出永远是对应的,不管外界如何变化,并不会改变其对应的结果。
const getResult = n => n * 2;

通过以上的比较,不难得出结论:一般尽可能的使用纯函数来保持函数本身不被外部所影响,从保持代码的可维护性与可迭代性。

3、高阶函数

仅仅通过上面的纯函数还远远不能体现函数式编程范式的优点,因为那只是基本要求而已,接下来的高阶函数才是打开函数式编程范式的大门。

什么叫高阶函数:当一个函数使用函数作为参数或返回值是一个函数的时候,这个函数就称为高阶函数。
还是以上面的那个案例来说,当你不确定到底是传过来的参数是2还是多少的时候,然后又不希望被外部的变量和常量影响的话,那可以使用高阶函数的形式封装它。

// 高阶函数方式
      const fn = num => {
            return (n) => num * n;
      }

        const num2 = fn(2);  // 返回一个*2的方法
        const num3 = fn(3);  //返回一个*3的方法

        const res1 = num2(100); 
        const res2 = num2(200);
        const res3 = num3(300);
        
        console.log(res1, res2, res3); // 结果为:200 400 900

仔细看这个代码就会发现我们使用一个高阶函数的形式将2或3这个这里的处理封装返回了一个可复用的函数。
同样还有使用函数作为参数的高阶函数,如:

  // 封装一个遍历操作数组每项的方法
    const arr = [1, 2, 3 ,4 ,5 ,6];
    
    const myForEach = (ctx, fn) => {
        for(let i = 0; i < ctx.length; i++) {
            fn(i);
        }
    }

    console.log(myForEach(arr, item => console.log(item) )); // 1 2 3 4 5 6
    console.log(myForEach(arr, item => console.log(item * 2) )); 2, 4, 6, 8, 10, 12

以上封装了一个myForEach的高阶函数,使得遍历数组和操作数组直接在形式上进行分开,让使用者只要把关注点放在“我需要输出数组的每一项或每一项*2之后再输出”,至于遍历这个操作已经被封装了,已经不关它的事了。像数组的map、forEach、reduce、filter等方法这些也是属于高阶函数,也是按照这种思路封装好的原生方法。

所以可以大概总结一下高阶函数的用法:
1、将本身不确定的因素抽离通过传参的形式返回一个可复用的符合当前因素的方法
2、将做怎么做这一部分代码抽离封装到一个可复用的函数中,将要做什么这一部分作为实参函数传递到该函数中,使得程序员只需要关注我要做什么,直接省去了程序员怎么做这一部分的工作与逻辑。

4、函数的柯里化

通过了解上面高阶函数之后就会发现一个很重要的思路那就是抽离,那可不可以也将函数的多个参数进行抽离分开使用使得在特定的时候使用参数更加灵活多变,如让一个求和函数进行处理让add(1, 2, 3)的结果和add(1)(2)(3)的运行结果是一样的呢。

这就得引入另一个概念了:函数的柯里化,是把接受多个参数的函数变换成接受一个单一参数的函数,其实就是一个颗粒化的过程。

就以上面提到的求和方法为例想要把add(1, 2, 3)的每个参数都单独抽离出来值参,直接使用上面高阶函数的思路先实现一个最简单的求和方法的柯里化。

const add = a => {
    return function (b) {
        return function(c) {
            return a + b + c;
        }
    }
}

console.log( add(1)(2)(3) );  // 输出为:6

以上这个方法就实现将求和方法的每个参数都单独抽离出来,但是这个例子只是为了更好的理解柯里化,并不推荐这么写,因为这个写法即不灵活也不优雅。所以我们可以在这个基础上改造一下。

const add = (...args) => {
    // 创建一个数组用来存放所有的参数
    const _args = [...args];

    const _add = function() {
        _args.push(...arguments);
        return _add;
    }

    // 重写toString方法
    _add.toString = function() {
        return _args.reduce((count, cur) => count + cur, 0);
    }

    return _add;
}

const res1 = add(1)(2)(3);
const res2 = add(1, 2)(3);
const res3 = add(1, 2, 3);
console.log(res1 + ''); // 使用字符串拼接的方式,使其触发toString,来获取计算的结果。
console.log(res2 + ''); 
console.log(res3 + ''); 

通过这个改造之后使用起来就灵活多了,可传一个,可同时传多个,就是最后需要转换成字符串触发最后的计算。 但是这个还是有缺陷,这个的求和是灵活了但是也仅仅只限于求和了,功能都限制死了复用性不高。所以还得改造一个通用的柯里化处理方法出来。

// 待柯里化的函数:多个数的求和方法
const _add = (...args) => args.reduce((count, item) => count + item, 0);


// // 柯里化的处理方法
const currying = fn => {
    return function curryFn(...args) {
        return function() {
            // 判断当前参数的个数,如果有参数则继续柯里化
            if(arguments.length) { 
                return curryFn(...args, ...arguments);
            } else {
                // 如果没有参数则开始计算
                return fn(...args);
            }
        }        
        
    }
}

const add = currying(_add); //  返回一个被柯里化的求和方法

console.log(add(1)(2)(3)()); // 输出结果:6
console.log(add(1, 2)(3)()); // 输出结果:6
console.log(add(1, 2, 3)()); // 输出结果:6

这样通过一个公共的柯里化的处理方法,可以将我们所需要的各种功能柯里化一个可复用的方法,同时还将柯里化过程进行抽离出来让使用者只关心自己的功能逻辑。

5、组合函数

当我们的功能越来越复杂的时候,往往需要抽离出许多方法,使得这些方法依赖上一个方法的运行结果。但是这些方法一多写起来也起来也不太优雅。用一个简单的例子:判断一串字符中所有数字字符的和是否为一个偶数,那按我们函数式的思路就可以抽离出三个方法:获取所有数字、求和、判断是否为偶数

const str = "dsfsj34jljlksdjf45df3534";

// 返回所有的数字,并转换成数字
const getNums = str => str.match(/\d/g).map(item => Number(item));

// 对所有数字求和 
const getSum = arrNums => arrNums.reduce((count, item) => count + item, 0);

// 判断是否为偶数 
const isEven = n => n %2 === 0;

// 1、这么写觉得麻烦 
const arrNumbs = getSum(str);
const sum = getSum(arrNumbs);
const even = isEven(sum);

// 2、这样写又不便于理解与维护
isEven(getSum(getNums(str)))

在这种情况下,我们可以创建一个通用的方法将这些函数组合起来去执行,然后将最后一步的计算结果返回出来,同样可以使得代码比较简练还便于理解维护。

// 创建一个组合函数执行的通用方法:按从左到右的顺序
const compose = (...fns) => {
    return function(x) {
        return fns.reduce((res, fn) => fn(res), x);
    }
}

const str = "dsfsj34jljlksdjf45df3534";

// 返回所有的数字,并转换成数字
const getNums = str => str.match(/\d/g).map(item => Number(item));

// 对所有数字求和 
const getSum = arrNums => arrNums.reduce((count, item) => count + item, 0);

// 判断是否为偶数 
const isEven = n => n %2 === 0 ? "偶数" : "非偶数";

// 生成一个从左到右依次执行 getNums getSum isEven的方法
const evenFn = compose(
    getNums, 
    getSum, 
    isEven
); 

console.log( evenFn(str) ); 

总结

随着项目的功能越来越多,封装的公共基础方法也越来越多,这时候函数式编程范式的优势就开始体现出来了,通过上面的方法可以将项目中的功能代码进行各种抽离优化,不止让代码变得更加简洁、优雅还能大大加强项目的后期可迭代性与可维护性。

以上这些只是一些基本的,未完待续……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容

  • 头等函数 当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数JS中的函数可以: 作为函数的参数 ...
    造车坊阅读 466评论 0 1
  • 第一章: 函数式编程主要基于数学函数和它的思想。 1.1 函数与js方法: 函数是一段可以通过其名称被调用的代码,...
    yuhuan121阅读 11,938评论 5 8
  • 函数式编程范式 为什么学习函数式编程 函数式编程是随着react的流行受到了越来越多的关注 vue 3也开始拥抱函...
    _咻咻咻咻咻阅读 195评论 0 0
  • 文章内容输出来源:拉勾教育大前端高薪训练营 和自我总结 学习函数式编程的意义 1.受React的流行而被人们越来越...
    油菜又矮吹阅读 405评论 0 0
  • 什么是函数式编程? 函数式编程是变成范式之一,我们常听说的编程范式还有面向过程式编程和面向对象式编程。 面向过程式...
    丽__阅读 860评论 1 3