【函数式编程】数组的函数式编程

数组的函数式编程

写在前面

本文的目的在于使用函数式编程解决常见问题(非命令式),以下的函数可能在数组或对象中有原型,建议理解其中的运行机制,而不是覆盖原生函数。本文仅代表个人对函数式编程的一些粗浅认识,仅供参考,如有错漏,欢迎指出。

第一部分 简单函数

一些常用的函数的函数式写法

forEach函数

最常见的数组函数,用于遍历数组,写法如下:

export const forEach = (arr, fn) => {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    fn ? result.push(fn(arr[i])) : undefined;
  }
  return result;
};
import { forEach } from "./utils";
const arr1 = [1, 2, 3];
const fn1 = (x) => x * x;
console.log(forEach(arr1, fn1)); // [1, 4, 9]

代码解释:
创建一个forEach函数,接受一个数组和一个函数作为参数,平时使用是Array.forEach,这里为了通用把数组传进去,下面的函数都是一样的方式,基本上都是forEach的变种。
forEach函数:定义result作为返回的数组,然后遍历传入的数组,判断传入的函数是否存在,如果存在则执行函数,并存储结果,最后返回结果数组
传入的函数是平方函数,最后结果是[1,4,9],这是一个比较简单的函数,也是比较重要的函数,它把对数组的操作简化为对单一数组项的操作,使逻辑更加容易理解。

map函数

map函数常见用来取数组中的某一项,和forEach函数大同小异,写法如下:

export const map = (arr, fn) => {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    fn ? result.push(fn(arr[i])) : undefined;
  }
  return result;
};
const arr2 = [
  { name: "小明", age: 16, class: "班级1" },
  { name: "小红", age: 17, class: "班级2" },
  { name: "小华", age: 16, class: "班级1" },
];
const fn2 = (x) => x.name;
console.log(map(arr2, fn2)); // [ '小明', '小红', '小华' ]

filter函数

如果想要筛选满足条件的数组项,可以把传入的方法写成一个判断方法,当满足此方法时才把值存储,写法如下:

export const filter = (arr, fn) => {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    fn && fn(arr[i]) ? result.push(arr[i]) : undefined;
  }
  return result;
};

const arr3 = [1, 4, 9];
const fn3 = (x) => x > 3;
console.log(filter(arr3, fn3)); // [4, 9]

every函数

如果想检查是否每一项都是满足条件的,可以设定默认值true,遍历判断每一项的结果和之前计算的结果取并集,或者用后面的reduce函数实现,方法有很多

export const every = (arr, fn) => {
  let result = true;
  for (let i = 0; i < arr.length; i++) {
    result = result && fn(arr[i]);
  }
  return result;
};
const arr4 = [1, 4, 9];
const arr5 = [-1, 1, 2];
const fn4 = (x) => x > 0;
console.log(every(arr4, fn4)); // true
console.log(every(arr5, fn4)); // false

这里这个方法还可以进一步优化,比如不需要判断每一项都满足,只想要一项不满足就可以退出,这里只讨论函数式编程的思想,对具体细节不做过多要求。

some函数

与every函数相反,只需要一个数组项满足即可,和every函数的实现也很像,写法如下:

export const some = (arr, fn) => {
  let result = false;
  for (let i = 0; i < arr.length; i++) {
    result = result || fn(arr[i]);
  }
  return result;
};
const arr4 = [1, 4, 9];
const arr5 = [-1, 1, 2];
const fn5 = (x) => x < 0;
console.log(some(arr4, fn5)); // true
console.log(some(arr5, fn5)); // false

sortBy函数

在写这个函数之前,我们先来了解Array的内置函数sort
官方定义:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
简单来说就是sort支持传入一个排序函数compareFn或者不传函数默认按Unicode先后排序,sort灵活的原因正是在于高阶函数的本质。
常见的compareFn可能是这样的:

const arr6 = [
  { name: "小江", age: 18, class: "班级3" },
  { name: "小红", age: 17, class: "班级2" },
  { name: "小华", age: 16, class: "班级1" },
];
const compareFn = (a, b) => {
  if (a.age < b.age) {
    return -1;
  }
  if (a.age > b.age) {
    return 1;
  }
  return 0;
  // 等价于
  // return (a.age < b.age) ? -1 :(a.age > b.age) ? 1 : 0
};
console.log(arr6.sort(compareFn));
// [
//   { name: "小华", age: 16, class: "班级1" },
//   { name: "小红", age: 17, class: "班级2" },
//   { name: "小江", age: 18, class: "班级3" },
// ];

有时候比较的属性不一样,就要再写一个排序函数,能不能只需要传入要比较的属性就行,像这样:

export const sortBy = (prop) => {
  return (a, b) => {
    if (a[prop] < b[prop]) {
      return -1;
    }
    if (a[prop] > b[prop]) {
      return 1;
    }
    return 0;
    // 等价于
    // return (a[prop] < b[prop]) ? -1 :(a[prop] > b[prop]) ? 1 : 0
  };
};
console.log(arr6.sort(sortBy("age")));
// [
//   { name: "小华", age: 16, class: "班级1" },
//   { name: "小红", age: 17, class: "班级2" },
//   { name: "小江", age: 18, class: "班级3" },
// ];

代码解释:sortBy函数接受一个参数,创建了一个有两个参数的函数,把要比较的属性传入,避免重复写逻辑类似的排序函数,这是高阶函数的一种应用。

flatten函数

对嵌套数组扁平化
实际开发中经常有这样的场景:统计不同班级的学生的成绩,第一步是获取学生信息,可能学生的信息是这样的,要整理成一个数组:

const student = [
  [
    { name: "小华", age: 16, score: 98 },
    { name: "小江", age: 17, score: 80 },
  ],
  [
    { name: "小红", age: 17, score: 58 },
    { name: "小明", age: 17, score: 75 },
  ],
];

对上述对象进行扁平化,调用flatten函数,写法如下:

export const flatten = (arr) => {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push.apply(result, arr[i]);
  }
  return result;
};

console.log(flatten(student));
// [
//   { name: "小华", age: 16, score: 98 },
//   { name: "小江", age: 17, score: 80 },
//   { name: "小红", age: 17, score: 58 },
//   { name: "小明", age: 17, score: 75 },
// ];

第二部分 复杂函数

函数组合

函数式编程的一个特点就是组合,把基础的函数像搭积木一样组合,能够实现比较复杂的功能。

举个栗子
比如想要统计不及格的学生,而学生信息如下:

const classes = [
  {
    class: "班级1",
    student: [
      { name: "小华", age: 16, score: 98 },
      { name: "小江", age: 17, score: 80 },
    ],
  },
  {
    class: "班级2",
    student: [
      { name: "小红", age: 17, score: 58 },
      { name: "小明", age: 17, score: 75 },
    ],
  },
];

简单的思路是这样的:先用map函数把student都取出来,形成一个嵌套数组,再用flatten函数扁平化数组,最后用filter函数筛选出不及格的学生,代码如下:

const fn7 = (clazz) => clazz.student;
const fn8 = (student) => student.score < 60;
console.log(map(classes, fn7));
// [
//   [
//     { name: "小华", age: 16, score: 98 },
//     { name: "小江", age: 17, score: 80 },
//   ],
//   [
//     { name: "小红", age: 17, score: 58 },
//     { name: "小明", age: 17, score: 75 },
//   ],
// ];
console.log(flatten(map(classes, fn7)));
// [
//   { name: "小华", age: 16, score: 98 },
//   { name: "小江", age: 17, score: 80 },
//   { name: "小红", age: 17, score: 58 },
//   { name: "小明", age: 17, score: 75 },
// ];
console.log(filter(flatten(map(classes, fn7)), fn8));
// [ { name: '小红', age: 17, score: 58 } ]

从这可以体会到函数式编程的本质,我们抽象出了数组的细节,并专注于问题本身。

reduce函数

说到函数式编程,不得不说到reduce函数,reduce函数最常见的应用场景就是像累加/阶乘这种统计性质的函数。reduce函数通常和闭包结合起来,展现Javascript的闭包能力。
我们先从简单的累加函数实现来说明reduce的作用
要计算一个数组的累加值,可以是这样:

// reduce 思路
const arr8 = [1, 3, 5, 7, 9];
let result = 0;
const fn8 = (value) => {
  result += value;
};
forEach(arr8, fn8);
console.log(result);

我们用函数包起来,并把变量改个名字

// reduce函数第一版
export const reduce1 = (arr, fn) => {
  let accumlator = 0;
  for (let i = 0; i < arr.length; i++) {
    accumlator = fn(accumlator, arr[i]);
  }
  return accumlator;
};
const arr9 = [1, 3, 5, 7, 9];
const fn9 = (acc, val) => acc + val;
console.log(reduce1(arr9, fn9)); // 25

简单说明一下,定义一个相加函数,在reduce函数中调用,表示为函数的值与每一项都相加,把最后的结果返回
这里有个问题,reduce的初始值accumlator为0,如果传入的函数是阶乘函数就抓瞎了,所以还需要有设置初始值,可以这样修改:

export const reduce2 = (arr, fn, initvalue) => {
  let accumlator = initvalue === undefined ? arr[0] : initvalue;

  if (initvalue === undefined) {
    for (let i = 1; i < arr.length; i++) {
      accumlator = fn(accumlator, arr[i]);
    }
  } else {
    for (let i = 0; i < arr.length; i++) {
      accumlator = fn(accumlator, arr[i]);
    }
  }
  return accumlator;
};

const arr10 = [1, 2, 3, 4, 5];
const fn10 = (acc, val) => acc + val;
const fn11 = (acc, val) => acc * val;
console.log(reduce2(arr10, fn10)); // 15
console.log(reduce2(arr10, fn10, 0)); // 15
console.log(reduce2(arr10, fn11, 1)); // 120

这里增加一个初始值initValue,还做了判断,如果不传initValue,则取数组第一个值,注意,取了第一个值后想要从第二个值开始遍历。

zip函数

合并两个指定数组
假如要给学生成绩评级,最后得到一个学生信息清单,已有信息如下:

const classes = [
  {
    class: "班级1",
    student: [
      { name: "小华", age: 16, score: 98 },
      { name: "小江", age: 17, score: 80 },
    ],
  },
  {
    class: "班级2",
    student: [
      { name: "小红", age: 17, score: 58 },
      { name: "小明", age: 17, score: 75 },
    ],
  },
];

const Honor = [
  { name: "小华", review: "优" },
  { name: "小江", review: "优" },
  { name: "小红", review: "良" },
  { name: "小明", review: "差" },
];

zip函数的定义:

export const zip = (leftArr, rightArr, fn) => {
  let result = [];
  for (let i = 0; i < leftArr.length; i++) {
    for (let j = 0; j < rightArr.length; j++) {
      if (fn(leftArr[i], rightArr[j]) !== undefined) {
        result.push(fn(leftArr[i], rightArr[j]));
      }
    }
  }
  return result;
};

简单说明一下,遍历两个数组,如果有满足要求的结果添加到清单中

const students = flatten(map(classes, fn7));
// [
//   { name: "小华", age: 16, score: 98 },
//   { name: "小江", age: 17, score: 80 },
//   { name: "小红", age: 17, score: 58 },
//   { name: "小明", age: 17, score: 75 },
// ];

const fn12 = (stu, honor) => {
  if (stu.name === honor.name) {
    let clone = Object.assign({}, stu);
    clone.review = honor.review;
    return clone;
  }
};
console.log(zip(students, honors, fn12));
// [
//   { name: "小华", age: 16, score: 98, review: "优" },
//   { name: "小江", age: 17, score: 80, review: "优" },
//   { name: "小红", age: 17, score: 58, review: "良" },
//   { name: "小明", age: 17, score: 75, review: "差" },
// ];

fn12定义了一个判断函数,如果一个学生数组能匹配到另一个学生数组,则把这个学生的信息记下,最后的结果就是学生信息合并

总结

数组的函数与遍历是分不开的,通过把遍历操作抽象出来,把对一个数组的操作转化成对每一个数组项的操作函数,能专注于问题本身,而一些基本的函数通过组合也可以实现复杂的操作,减少理解代码的成本。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值