javascript函数式编程基础

281 阅读7分钟

函数式编程概念

程序的本质是根据输入得到输出。x -> f(联系、映射) -> y,y=f(x)。

函数是一等公民,就是一个普通的对象。它能作为另一个函数的参数,也能作为一个函数的返回值,还可以当作变量存储

//  非函数  
const num1 = 2;  
const num2 = 3;  
const sum = num1 + num2;  
// console.log(sum)

//  函数式(函数表达式,将函数作为一个变量赋值)  

const add = (num1, num2) => { 
    return num1 + num2;  
}  
// console.log(add(num1, num2))

高阶函数

高阶函数可以让我们用来公用通用的方法(forEach,map,reduce,some,every,find,findIndex)

满足高阶函数的条件必须达到其中之一:

1.一个函数的参数可以是另一个函数  
2.函数的返回值为另一个函数

//  函数作为参数,如下forEach,filter

//  手写forEach函数
const forEach = (array, fun) => {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        fun(element)
    }
}
forEach([1,2,3,4,5], num => console.log(num * 2))
// 2 4 6 8 10

//  手写filter函数
const filter = (array, fun) => {
    const result = [];
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        if (fun(element)) result.push(element)
    }
    return result; 
}
console.log(filter([1,2,3,4,5], num => num % 2 === 0))
//  [ 2, 4 ]

//  函数作为返回值,如makeFn,once
const makeFn = () => {
    const msg = 'hello function';
    return _ => console.log(msg);
}
makeFn()()
//  hello function

//  只会付一次钱
const once = fn => {
    let done = false;
    return function() {
        if (!done) {
            done = true;
            fn.apply(this, arguments)
        }
    }
}

const pay = once((money) => {
    console.log(`支付${money}元钱`)
})
pay(5)
pay(5)
pay(5)

支付5元钱

闭包函数

以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员(外部访问一个函数的内部函数,这个内部函数引用了函数的成员)

闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

上面的makeFn、once都用到了闭包,闭包必然是一个高阶函数,因为把函数作为了返回值

/*  例如求一个数的n次方,如果我有大量的值需要求2次方,则我们可以先生成一个2次方
的函数,接着传所需的值。这里,外层函数的参数power在makePower执行完毕后,并没有
被销毁,内部函数还要继续使用,所以就形成了闭包
*/
const makePower = (power) => num => Math.pow(num, power);

const power2 = makePower(2);
const power3 = makePower(3);

console.log(power2(3))
console.log(power3(3))

纯函数

相同的输入永远会得到相同的输出,而且没有任何可观察的副作用(意思就是外部的状态),纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x) image.png

//  不纯函数,mini是一个外部的配置属性,如果改变了,则不满足纯函数的条件
const mini = 18;
const checkAge = age => age > mini;

//  修改成纯函数
const checkAge = age => {
    const mini = 18;
    return age > age;
}

副作用

副作用会让你个函数变得不纯,副作用不可能完全禁止,尽可能控制他们在可控范围内发生,如上述纯函数的例子。

副作用的来源:
1.配置文件
2.数据库
3.用户的输入
......

柯里化

当一个函数有多个参数的时候,这时候可以先传递一部分参数调用,然后返回一个新的函数接受剩余的参数,直到参数接受完毕,返回结果

相当于对一个多参数的函数分批次执行,我们可以得到其中的函数缓存

特点:
1.柯里化可以让我们给函数传递较少的参数得到一个已经记住了某些固定参数的新函数
2.这是一种对函数参数的缓存
3.让函数变得更灵活,让函数得颗粒度更小
4.可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

//  柯里化检查年龄函数
const checkAge = mini => age => age > mini;
const check18 = checkAge2(18);
console.log(check18(20));
//  true
/*  自己实现柯里化,首先参数是一个函数,这个函数的参数有N个,且返回值也是一个函
数,当这个函数被柯里化后,调用这个函数,如果参数小于N个,则继续返回一个函数,等
待剩余参数得传递,如果参数等于N个了,则直接返回最终结果
*/
const curry = fn => {
    return function curriedFn(...args) {
        // 判断实参跟形参的个数
        if(args.length < fn.length){
            return function() {
                return curriedFn(...args, ...arguments);
            }
        }
        return fn(...args);
    }
}

const ownerCurriedFilter = curry((fn, array) => array.filter(fn))

//  两种方式得到的结果一样
console.log(ownerCurriedFilter(num => num % 2 === 0, [1, 2, 3, 4]))
console.log(ownerCurriedFilter(num => num % 2 === 0)([1, 2, 3, 4]))
//  [ 2, 4 ]

函数组合

如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

特点:
1.函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
2.函数组合默认从右到左执行
3.函数组合满足结合律

const reverse = array => array.reverse();
const first = array => array[0];
const toUpper = str => str.toUpperCase();

//  这里用到了lodash库
const flowRightFn = _.flowRight(toUpper, first, reverse);
console.log(flowRightFn(['aaa', 'bbb', 'ccc']));
//  CCC
/*  实现函数组合,首先传递的参数是一组函数,返回的是一个函数,特点就是上一个函
数执行的返回值是下一个函数的参数,类似管道形式往下传递,直到所有函数运行完返回
结果
*/
const compose = (...args) => {
    return function(value) {
        return args.reverse().reduce((current, fn) => {
            return fn(current)
        }, value)
    }
}
const flowRightFn = compose(toUpper, first, reverse);
console.log(flowRightFn1(['aaa', 'bbb', 'ccc']));
//  CCC

lodash是一个非常实用的函数库,对字符串、数组、对象的处理等函数都是纯函数,其中的fp模块中的函数都是被柯里化的,可以直接用。熟练使用lodash库可以提升开发效率

pointfree模式是一种编程风格,就是函数的组合

特点:
1.不需要指明处理的数据
2.只需要合成运算的过程
3.需要定义一些辅助的基本运算函数

Funtor函子

目的是在函数式编程中如何把副作用控制在可控范围内,异常处理,异步操作等。个人理解就是一个盒子,这个盒子里存放了一个值,这个值对外是不可见,要获取这个值,或者操作这个值都需要特定执行才能拿到,且处理完这个值之后则继续返回一个盒子,这个盒子里装的是你新的值。

特点:
1.函数式编程的运算不直接操作值,而是由函子完成
2.函子就是实现; map契约的对象
3.我们可以把函子想象成一个盒子,盒子里封装了一个值
4.想要处理盒子中的值,我们需要给盒子的map防滑传递一个处理值的纯函数,有这个函数来对值进行处理
5.最终map方法返回一个包含新值的函子

//  一个最基本的函子,一个容器,包裹一个值
class Container {
    //  of静态方法,可以省略new关键字创建对象
    static of (value) {
        return new Container(value)
    }

    constructor(value) {
        this._value = value
    }
    //  map方法,传入函数,将容器里的每一个值映射到另一个容器
    map(fn) {
        return Container.of(fn(this._value));
    }
}

Container.of(2)
.map(x => x + 2)
.map(x => x * x)

IO函子

特点:
1.IO函子中的_.value是一个函数,这里把函数作为值来处理
2.IO函子可以把不纯的动作储存到_value中,延迟执行这个不存的操作,保证当前的操作纯
3.把不存的操作交给调用者来处理

/*  _fn.flowRight是lodash中的函数组合,所以我们想处理_value函数返回的值,我们
    可以写在调用的任何地方,因为函数组合都会最先处理_value的函数
*/
class IO {
    static of() {
        return new IO(function() {
            return x
        })
    }
    constructor(fn) {
        this._value = fn;
    }

    map(fn) {
        //  把当前的value和传入的fn组合成一个新的函数
        return new IO(_fn.flowRight(fn, this._value))
    }
}