数组的函数式编程
写在前面
本文的目的在于使用函数式编程解决常见问题(非命令式),以下的函数可能在数组或对象中有原型,建议理解其中的运行机制,而不是覆盖原生函数。本文仅代表个人对函数式编程的一些粗浅认识,仅供参考,如有错漏,欢迎指出。
第一部分 简单函数
一些常用的函数的函数式写法
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定义了一个判断函数,如果一个学生数组能匹配到另一个学生数组,则把这个学生的信息记下,最后的结果就是学生信息合并
总结
数组的函数与遍历是分不开的,通过把遍历操作抽象出来,把对一个数组的操作转化成对每一个数组项的操作函数,能专注于问题本身,而一些基本的函数通过组合也可以实现复杂的操作,减少理解代码的成本。