函数式编程

252 阅读10分钟

函数式编程

持续更新,文章内容较长,建议收藏食用,如果觉得内容可以,求点赞哦。


什么是函数式编程

函数式编程(Functional Programming)简称FP,FP是编程范式之一,其他常见的编程范式还有面向过程编程、面向对象编程。

  • 面向过程(Procedure Oriented)的思维方式:是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响。
  • 面向对象(Object Oriented)的思维方式:把现实世界中的事物抽象成一个个类和对象,通过封装、继承和多态来演示事物的联系。
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系映射到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入通过某种运算获得相应的输出
    • x->f(联系,映射)-<y,y=f(x)
    • 函数式编程中的函数指的不是程序中的函数(方法),而是指映射关系
    • 相同的输入始终得到相同的输出(纯函数)
// 非函数式
let a = 1;
let b = 2;
let sum = a + b;
console.log(sum);

// 函数式
function add(a, b) {
	return a + b;
}
let sum = add(1, 2);
console.log(sum);

相较于非函数式,函数式一个很明显的优点:可复用


高阶函数

什么是高阶函数

  • 高阶函数(Higher-order function)
    • 可以把函数作为参数传给另一个函数
    • 可以把函数作为另一个函数的返回值
  • 作为参数
    • 代码
// 高阶函数-作为参数
// 实现forEach

function forEach(array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i]);
    }
}

let arr = [1, 2, 3, 4, 5];
forEach(arr, item => {
    console.log(item)
});

// 实现filter

function filter(array, fn) {
    let result = [];
    for (let i = 0; i < array.length; i++) {
        if (fn(array[i])) {
            result.push(array[i]);
        }
    }
    return result;
}

let arr2 = [1, 2, 3, 4, 5, 6];
let result = filter(arr, item => item % 2 === 0);
console.log(result);
  • 输出 在这里插入图片描述

  • 作为返回值

// 高阶函数-作为返回值

//实现once函数
function once(fn) {
    let flag = false;
    return function() {
        if (!flag) {
            flag = true;
            fn(...arguments);
        }
    }
}

let pay = once(function(money) {
    console.log(`支付了:${money}RMB`);
});
pay(5);
pay(5);
pay(5);
  • 输出 在这里插入图片描述 只执行了1次

高阶函数的意义

  • 可以帮助我们屏蔽细节,只需要关注需要的结果
  • 用来抽象通用的问题

常用的几个高阶函数

手动实现map、every、some函数

// map
const map = (array, fn) => {
    let result = [];
    for (let item of array) {
        result.push(fn(item));
    }
    return result;
}
let arr = [1, 2, 3, 4, 5];
let r = map(arr, item => item * 3);
console.log(r); // => [3, 6, 9, 12, 15];
// every
const every = (array, fn) => {
    let result = true;
     for(let value of array) {
         result = fn(value);
         if (!result) break;
         console.log(12112)
     }
     return result;
}
let arr = [1, 2, 3, 4, 5];
let r = every(arr, item => item > 3);
console.log(r); // => false
// some
const some = (array, fn) => {
    let result = false;
    for(let value of array) {
        result = fn(value);
        if (result) break;
    }
    return result;
}
let arr = [1, 2, 3, 4, 5];
let r = some(arr, item => item > 3);
console.log(r); // => true

闭包

闭包是什么

闭包(Closure):就是能够读取其他函数内部变量的函数。

  • 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员。
  • 可以延长变量的作用范围。

闭包案例

计算不同职级工资,可以在浏览器在自行断点,查看是否存在闭包。(代码如下)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, inittial-scale=1.0">
        <title>closure</title>
    </head>
    <body>
        <script type="text/javascript">
            // 计算不同职级的工资
            function getSalara(level) {
                return function(baseSalara) {
                    return baseSalara + level;
                }
            }
            const salara1 = getSalara(2000); //可以在此处打断点观察闭包情况
            const salara2 = getSalara(3000);
            console.log(salara1(15000));
            console.log(salara1(18000));
            console.log(salara2(20000));
        </script>
    </body>
</html>

在这里插入图片描述


纯函数和非纯函数

纯函数是什么?

  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可见的副作用。
    • 纯函数:slice 代码如下(示例):
let array = [1, 2, 3, 4, 5];

// 纯函数-slice
console.log(array.slice(0, 3));
console.log(array.slice(0, 3));
console.log(array.slice(0, 3));

在这里插入图片描述 相同的输入(0, 3)永远得到相同的输出结果[1, 2, 3];

  • 非纯函数:splice 代码如下(示例):
let array = [1, 2, 3, 4, 5];

// 非纯函数-splice
console.log(array.splice(0, 3));
console.log(array.splice(0, 3));
console.log(array.splice(0, 3));

在这里插入图片描述

输入(0, 3)相同,但是输出结果不同。

纯函数的好处

  • 可缓存
    • 因为纯函数对相同的输入始终有相同的输出,所以可以纯函数的结果缓存起来。
  • 可测试
    • 纯函数让测试后更方便,因为纯函数有输入和输出,并且相同的输入总是有相同的输出,而单元测试就是断言函数的输出。
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况。
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数。

案例

lodash中记忆函数memoize(可缓存)

const _ = require('lodash');

function getArea(r) {
    console.log(r);
    return Math.PI * r * r;
}

let getAreaWithMemoize = _.memoize(getArea);

console.log(getAreaWithMemoize(4));
console.log(getAreaWithMemoize(4));
console.log(getAreaWithMemoize(4));

在这里插入图片描述

可以看到,只有第一次调用执行了getArea方法,剩下两次都是取得缓存数据。

手动实现memoize函数

// 手动实现memoize

function memoize(fn) {
    let cache = {};
    return function() {
        let key = JSON.stringify(...arguments);
        if (!cache[key]) {
            cache[key] = fn(...arguments);
        }
        cache[key] = cache[key] || fn(...arguments);
        return cache[key];
    }
}
function getArea(r) {
    console.log(r);
    return Math.PI * r * r;
}

let getAreaWithMemoize = memoize(getArea);

console.log(getAreaWithMemoize(4));
console.log(getAreaWithMemoize(4));
console.log(getAreaWithMemoize(4));

在这里插入图片描述 输出结果一样。

副作用

副作用让一个函数变得不纯,纯函数是相同的输入永远得到相同的输出,但是如果函数依赖外部的变量就无法保证相同的输出,从而导致函数变得不纯

// 不纯的函数

let min = 18;
function checkAge1(age) {
    return age >= min;
}

// 纯函数

function checkAge2(age) {
    let min = 18;
    return age >= 18;
}

mn=18的条件下,checkAge1(20) 返回true,在mn=22的条件下,checkAge1(20) 返回false,因为checkAge1函数依赖了外部变量min,如果min发生改变,就会导致相同的输入,得不到相同的输出,机会导致函数不纯。相反,checkAge2就是一个纯函数,因为它不依赖外部变量。

副作用的来源

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ...

柯里化

什么是柯里化

  • 柯里化(Curry)
    • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)。
    • 然后返回另一函数来接收剩余的参数,并返回结果。
    • 可以将多元函数处理成一元函数
    • 如果传入所有参数,则会立即执行;如果传入一部分参数,则会返回一个函数来等待接收剩余的参数。

案例

function checkAge(age) {
    let min = 18;
    return age > min;
}

// 普通纯函数

function checkAge(min, age) {
    return age >= min;
}
console.log(checkAge(18, 20));
console.log(checkAge(18, 22));
console.log(checkAge(18, 24));
// 柯里化
//如果我们的函数里面经常要用到18这个参数,那么我们调用的时候需要不停的传入18。而通过将函数柯里化处理,我们只需要传入需要处理的参数。
function checkAge(min) {
    return function(age) {
        return age >= min;
    }
}

// es6写法

const checkAge = min => (age => age >= min)
const checkAge18 = checkAge(18);
console.log(checkAge18(20));
console.log(checkAge18(22));
console.log(checkAge18(24));

// lodash中的柯里化函数
const _ = require('lodash');

function getSum(a, b, c) {
    return a + b + c;
}

const curried = _.curry(getSum);

console.log(curried(1, 2, 3)); => 6
console.log(curried(1)(2, 3)); => 6
console.log(curried(1, 2)(3)); => 6

// lodash中的柯里化案例

const _ = require('lodash');

// 普通函数

const match = function(reg, str) {
    return str.match(reg);
}

// console.log(match(/\s+/g, 'helloworld')); // => null,需要传入2个参数

// 柯里化

const match = _.curry((reg, str) => str.match(reg));

const haveSpace = match(/\s+/g);

console.log(haveSpace('helloworld')) // => null,通过柯里化,只需要传入1个参数

const filter = _.curry((func, array) => array.filter(func));

const findSpace = filter(haveSpace);

console.log(findSpace(['helloworld', 'John Mary', 'Lebron James'])); // => ['John Mary', 'Lebron James']

模拟实现lodash中柯里化函数curry

先来梳理一下curry做了什么。首先,curry接收一个参数,这个参数是一个函数,假定为func,然后返回一个函数function,如果返回的这个函数function接收的参数(实参)个数大于等于func的定义的参数(形参)个数,那么直接执行func并返回结果;如果返回的这个函数function接收的参数(实参)个数小于func的定义的参数(形参)个数,那么返回一个函数,等待接收剩余的参数,直到前面传入的参数个数+后来传入的参数个数=func的参数个数时,执行func并返回结果。

function curry(func) {
    return function curried(...args) {
        if (args.length < func.length) {
            return function() {
                return curried(...args.concat(Array.from(arguments)));
            }
        }
        return func(...args);
    }
}
const match = curry((reg, str) => str.match(reg));
// 判断字符串是否包含空格
const haveSpace = match(/\s+/g);

console.log(haveSpace('helloworld')) // => null

函数组合

  • 函数就像是数据的管道,函数组合可以把这些函数组合起来,并输出最终的值
  • 函数组合是从右到左执行的
  • 函数组合要满足结合律f((a, b), c) ==f(a, (b, c))

纯函数和柯里化很容易写出洋葱代码,什么是洋葱代码呢,就是函数一层嵌套一层,一层包一层。举个例子

  • 获取数组的最后一个元素并转化成大写_.toUpper(_.first(_.reverse(array)))。先把数组翻转,然后获取数组的第一个元素,再转化成大写,一层包一层,这就是洋葱代码。而通过函数组合,可以避免书写出洋葱代码。函数组合可以把细粒度的函数重新组合生成一个新的函数。

管道

a通过fn函数处理之后,变成了b。那么这个fn就可以理解成一个管道。但是当这个管道太长的话,如果处理出现错误,就会不方便我们定位问题。比如家里的水管,如果这个水管太长,出现漏水的现象,就无法快速的定位到漏洞在哪里。这跟我们代码调试是一个道理,你是愿意调试10000行的代码,还是愿意调试10个100行的代码呢?通过函数组合就可以把很长的代码,拆分成若干个小的函数然后连接起来,这样当代码出现问题的时候,调试起来就会方便很多。下面我们通过函数组合来实现上面的例子:将数组的第一个元素转化成大写。

// 函数组合
function compose(f, g) {
    return function(value) {
        return f(g(value))
    };
}

// 翻转数组
function reverse(array) {
    return array.reverse();
}

// 获取第一个元素
function first(array) {
    return array[0]
}

const last = compose(first, reverse);

console.log(last(['a', 'b', 'c', 'd'])); // => D

这只是一个非常简单的案例,其实我们可以通过函数组合,组合更多的函数,实现更加复杂的功能。

lodash中的组合函数

接下来我们用lodash提供的方法来组合函数,实现将数组的第一个元素转化成大写

// lodash中的组合函数_.flowRight()

// lodash中的组合函数_.flowRight()

const _ = require('lodash');

const reverse = arr => arr.reverse();

const first = arr => arr[0];

const toUpper = str => str.toUpperCase();

const fn = _.flowRight(toUpper, first, reverse);

console.log(fn(['a', 'b', 'c', 'd'])); // => D

// 满足结合律

const fn1 = _.flowRight(_.flowRight(toUpper, first), reverse);
const fn2 = _.flowRight(toUpper, _.flowRight(first, reverse));

console.log(fn1(['a', 'b', 'c', 'd'])); // => D
console.log(fn2(['a', 'b', 'c', 'd'])); // => D

模拟实现lodash中的flowRight

先定义一个函数,传入我们需要组合的函数,然后返回一个函数。这个返回的函数的返回值就是我们要的结果。然后通过累加器函数reduce来依次执行传入的函数,由于reduce是将数组中的值从左到右递减,并计算出最终的值,而函数组合是从右到左执行的,所以我们要先把传入的参数做一个倒序,然后在reduce中传入2个参数,第一个参数是一个函数,用来返回经过函数组合处理之后的值,这个函数接收2个参数,第一个就是每次处理过后的值,第二个就是args中的每个函数,reduce的第二个参数就是我们需要处理的值。

// 模拟实现lodash中的flowRight

const reverse = arr => arr.reverse();

const first = arr => arr[0];

const toUpper = str => str.toUpperCase();

// function compose(...args) { // 因为函数组合传入的参数个数不固定,所以这里用es6...来展开入参
//     return function(value) {
//         return args.reverse().reduce(function(acc, fn) {
//             return fn(acc)
//         }, value)
//     }
// }
const compose = (...args) => (value => args.reverse().reduce((acc, fn) => fn(acc), value)); // es6写法

const fn = compose(toUpper, first, reverse);

console.log(fn(['a', 'b', 'c', 'd'])); // => D