函数式编程学习总结

107 阅读4分钟

函数式编程

  • 清晰表达数据流,让未来的自己或者未来的同事,更容易理解你的思路。
  • 遵循函数式编程原则,让代码更符合预期。

最原始的函数

最原始的函数,是数学中的函数,它表达的是某种规律的映射,入参和出参之间的关系。

隐式和显式模式

我们大部分开发者更喜欢显式,因为显式模式下,能保证数据的纯净,所以显式模式作为函数式的原则。

隐式其实也是叫:副作用, 隐式大部分是来自于引用的复制和使用,所以我们很容易写出隐式模式的函数。

例子:

function sum(list) {
    var total = 0;
    for (let i = 0; i < list.length; i++) {
        if (!list[i]) list[i] = 0; // list 使用了 nums 的引用,不是对 [1,3,9,..] 的值复制,而是引用复制。

        total = total + list[i];
    }

    return total;
}

var nums = [ 1, 3, 9, 27, , 84 ];

sum( nums );            // 124
console.log(nums) // [1, 3, 9, 27, 0, 84] // 函数隐式变更了数组引用的数据

具名函数

我个人是很推崇使用具名函数的,箭头函数除外。具名有利于语义化代码,并且在某些场景下,具名函数可以提高拓展性,例如类的方法声明,如果使用匿名是无法做到引用循环的,具名函数可以。

尽可能不使用this

因为js的this受调用影响比较大,有时候还未开始理解函数,就要花些时间理解this指向。并且,this属于引用,也不符合我们显式模式的原则。

高阶函数

定义:一个函数如果可以接受或返回一个甚至多个函数,它被叫做高阶函数。

对函数进行包装,使其成为一个高阶函数是函数式编程的精髓!

高阶函数的强大在于:【闭包】。

高阶函数分为两类:偏函数应用、柯里化。

偏函数应用

原则:入参最理想的情况下只需一个

秉承着原则,我们会利用偏函数将多个入参改为一个。

原理:利用高阶返回函数,通过一层额外的函数包装层,将函数声明和入参包装成一个执行函数。

优点:当函数只有一个形参时,我们能够比较容易地组合它们,这种单元函数,便于进行后续的组合函数。

例子:

// 偏函数
function partial(fn,...presetArgs) {
    return function partiallyApplied(...laterArgs){
        return fn( ...presetArgs, ...laterArgs );
    };
}

var getDemo = partial(ajax, "http://api/demo");
var getSingleDemo = partial(getDemo, { id: DEMO_ID });

// 两个函数的内部运行机制
var getDemo = function partiallyApplied(...laterArgs) {
    return ajax("http://api/demo", ...laterArgs);
};
var getSingleDemo = function outerPartiallyApplied(...outerLaterArgs) {
    var getDemo = function innerPartiallyApplied(...innerLaterArgs){
        return ajax("http://api/demo", ...innerLaterArgs);
    };

    return getDemo({ id: DEMO_ID }, ...outerLaterArgs);
}

柯里化

函数柯里化实际上是一种特殊的偏函数,所以优点和原则和偏函数一样。

将一个函数从 f(a, b, c) 转换为 f(a)(b)(c)。

原理:柯里化函数:接收单一实参(实参个数:1)并返回另一个接收下一个实参的函数。

例子:

// arity = fn.length 获取形参数量
function curry(fn,arity =  = fn.length) {
    return (function nextCurried(prevArgs){
        return function curried(nextArg){
            var args = prevArgs.concat([nextArg]);
						
          	// 接受到函数形参数量后,执行函数。
            if (args.length >= arity) {
                return fn(...args);
            }
            else {
              	// 将入参数数组传递到下一个函数
                return nextCurried(args);
            }
        };
    })([]);
}
function curry(fn, arg)

“偏函数” partial(sum,1,2)(3) “柯里化” sum(1)(2)(3)

组合函数

  • 组合函数,实际是利用单一原则做实现,封装组合使用。将每个单一功能的函数看作积木,我们可以自由组合搭建。
  • 在组合函数时,我们会发现,代码像流水线一样被组合在一个函数里,类似工厂。我们作为函数管理者,我们可以优化、调整内部流水线,只要保证进出不变,所以函数式也是种可控的质量方案。

例子:

别人给了我们一个西瓜,告诉我们,他要喝西瓜汁。

  1. 首先切开西瓜
  2. 将西瓜榨汁
  3. 将西瓜汁装到杯子里
  4. 将西瓜汁递给他

封装盒子

  • 组合函数,是将单一模块变成一个流程功能,那么封装是将这个流程做通用化,让他适配更多场景。
  • 本质上还是组合函数,之前是将模块组合成功能,现在将功能组合成场景。

例子:

别人给了我们一车瓜果,告诉我们,他要开果茶店,要我们给他弄一批各式各样到果汁。

  1. 将「果」切开
  2. 将切好到「果」榨汁
  3. 将「果」汁装杯
  4. 给到别人
  • compose 就是封装盒子,在前端也有比较广泛的应用。

compose

循环

function compose(...fns) {
    return function composed(result){
        // 拷贝一份保存函数的数组
        var list = fns.slice();

        while (list.length > 0) {
            // 将最后一个函数从列表尾部拿出
            // 并执行它
            result = list.pop()( result );
        }

        return result;
    };
}

递归

  • 属于一种compose变体
  • 与循环相比,递归有更好的循迹性,不过这种感觉,需要长时间的体会。
// ES6 箭头函数形式写法
var compose =
    (...fns) => {
        // 拿出最后两个参数
        var [ fn1, fn2, ...rest ] = fns.reverse();
        
        // 将 fn1 的执行结果传递给 fn2
        var composedFn =
            (...args) =>
                fn2( fn1( ...args ) );

        if (rest.length == 0) return composedFn;
        
        // 下一次执行,composedFn 就是 fn2 了。
        return compose( ...rest.reverse(), composedFn );
    };

抽象能力

抽象是一个过程,程序员将一个名字与潜在的复杂程序片段关联起来,这样该名字就能够被认为代表函数的目的,而不是代表函数如何实现的。通过隐藏无关的细节,抽象降低了概念复杂度,让程序员在任意时间都可以集中注意力在程序内容中的可维护子集上。—— 《程序设计语言》

  • 抽象不能太过,也不能不够,程度要适中。说的通俗点,这就是代码的艺术的核心。

副作用

// 片段 1
function foo(x) {
    return x * 2;
}

var y = foo( 3 );

// 片段 2
function foo(x) {
    y = x * 2;
}

var y;

foo( 3 );
// 调用foo,改变了y,这就是副作用
  • 有副作用当函数可读性很低,你需要理解其内部的实现才能理解程序。
  • 副作用回增加我们理解程序的成本,不利于提效。

如何避免副作用

  • 尽量利用 const 固定值,避免被窜改。
  • 尽量保证有 I/O ,确保函数执行有进有出。
  • 明确依赖,函数内部的依赖,是否存在先决条件。
  • 我们通常都会写一些副作用的函数,因为它是的可控的,我们是要避免副作用,而非远离它,有个尺度。

纯函数

  • 没有副作用的函数叫纯函数
  • 函数的纯度是和自信是有关的,函数越纯洁越好。制作纯函数时越努力,当您阅读使用它的代码时,你的自信就会越高,这将使代码更加可读。

数组三剑客

  • map
  • filter
  • reduce

高级用法

  • 去重
var unique =
    arr =>
        arr.filter(
            (v,idx) =>
                arr.indexOf( v ) == idx
        );

unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );  
  • 降维
[ [1, 2, 3], 4, 5, [6, [7, 8]] ] => [ 1, 2, 3, 4, 5, 6, 7, 8 ]

var flatten =
    arr =>
        arr.reduce(
            (list,v) =>
                list.concat( Array.isArray( v ) ? flatten( v ) : v )
        , [] );
  • 融合
var removeInvalidChars = str => str.replace( /[^\w]*/g, "" );

var upper = str => str.toUpperCase();

var elide = str =>
    str.length > 10 ?
        str.substr( 0, 7 ) + "..." :
        str;

var words = "Mr. Jones isn't responsible for this disaster!"
    .split( /\s/ );

words;
// ["Mr.","Jones","isn't","responsible","for","this","disaster!"]

// 片段 1
words
.map( removeInvalidChars )
.map( upper )
.map( elide );
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]

// 片段 3
words
.map(
    compose( elide, upper, removeInvalidChars )
);
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]
  • 片断3的融合,是一种常见的性能优化方式,将3次map改成1次map。