JavaScript 数据处理 - 列表篇

幽沉谢世事,俯默窥唐虞。这篇文章主要讲述JavaScript 数据处理 - 列表篇相关的知识,希望能为你提供帮助。
程序中的常用数据集合无非两类,列表 (List) 和映射 (Map)。在 javascript 的语言基础中就提供了这两种集合结构的支持 —— 用数组 (Array) 表示列表,用直接对象 (Plain Object) 表示映射(属性键值对映射)。
今天我们只说数组。从 ??Array??? 类中提供的实例方法可以看出来,数组涵盖了一般的列表操作,增删改查俱全,更提供了 ??shift()/unshift()??? 和 ??push()/pop()?? 这样的方法,使数组具有队列和栈的基本功能。除了日常的 CRUD 之外,最重要的就是对列表进行完全或部分遍历,拿到预期的结果,这些遍历操作包括

  1. 逐一遍历:??for???、??forEach()???、??map()?? 等;
  2. 筛选/过滤:??filter()???、??find()???、??findIndex()???、??indexOf()?? 等;
  3. 遍历计算(归约):??reduce()???、??some()???、??every()???、??includes()?? 等。
Array 对象提供来用于遍历的实例方法,大多数都是接收一个处理函数在遍历过程中对每个元素进行处理。而且处理函数通常会具有三个参数:??(el, index, array)???,分别表示当前处理的元素、当前元素的索引以及当前处理的数组(即原数组)。当然,这里说的是大多数,也有一些例外,比如 ??includes()??? 就不是这样,而 ??reduce??? 的处理函数会多一个表示中间结果的参数。具体情况不用多说,??查阅 MDN?? 即可。
一、简单遍历大家都知道 ??for??? 语法在 javaScript 中除了基本的 ??for ( ; ; )??? 之外,还包含了两种 for each 遍历。一种是 ??for ... in??? 用来遍历键/索引;另一种是 ??for ... of??? 用来遍历值/元素。两种 for each 结构都不能同时拿到键/索引和值/元素,而 ??forEach()??? 方法可以拿到,这是 ??forEach()??? 的便利所在。不过在 for each 结构中要终止循环,可以使用 ??break???,而在 ??forEach()??? 中要想终止循环只能通过 ??throw???。使用 ??throw??? 来终止循环需要在外面进行 ??try ... catch?? 处理,不够灵活。举例:
try
list.forEach(n =>
console.log(n);
if (n > = 3)throw undefined;
);
catch
console.log("The loop is broken");

如果没有 ??try ... catch???,里面的 ??throw?? 会直接中断程序运行。
当然,其实也有更简单的方法。注意到 ??some()??? 和 ??every()??? 这两个方法都是对数组进行遍历,直到遇到符合条件/不符合条件的元素。简单地说它们是根据处理函数的返回值来判断是否中断遍历。对于 ??some()??? 来说,是要找到一个符合条件的元素,处理函数如果返回 ??true???,就中断遍历;而 ??every()??? 正好相反,它是要判断每个元素都符合条件,所以只要遇到返回 ??false?? 就会中断遍历。
根据我们对一般 for 循环和 while 循环的理解,都是条件为真是进行循环,所以看起来 ??every()??? 更符合习惯。上面的示例用 ??every()?? 改写:
list.every(n =>
console.log(n);
return n < 3;
);

使用 ??some()??? 和 ??every()??? 特别需要注意一点:它不需要精确返回 boolean 类型的值,只需要判断真值 (truthy) 和 假值(falsy) 即可。 JavaScript 函数在没有显式返回值的情况下等同于 ??return undefined???,也就是返回假值,效果和 ??return false?? 等同。
关于 JavaScript 的假值,可以查阅 ??MDN - Falsy??。除了假值,都是真值。
二、遍历映射有时候我们需要对一个数组进行遍历,根据其每个元素提供的信息,产生另一个数值和对象,而结果仍然放在一个数组中。前端开发中这种操作最常见的场景就是将从后端拿到的?模型数据?列表,处理成前端呈现需要的?视图数据?列表。常规操作是这样:
// 源数据
const source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 创建目标数组容器
const target = [];
// 循环处理每一个源数据元素,并将结果添加到目标数组中
for (const n of source)
target.push( id: n, label: `label$n` );


// 消费目标数组
console.log(target);

【JavaScript 数据处理 - 列表篇】??map()??? 就是用来封装这样的遍历的,它可以用来处理一对一的元素数据映射。上例改用 ??map()?? 只需要一句话代替循环:
const source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const target = source.map(n => ( id: n, label: `label$n` ));
console.log(target);

除了减少语句之外,使用 ??map()?? 还把原来的若干语句,变成了一个表达式,可以灵活地用于上下逻辑衔接。
三、处理多层结构 - 展开 (flat 和 flatMap)展开,即 ??flat()?? 操作可以把多维度的数组减少 1 个或多个维度。举例来说
const source = [1, 2, [3, 4], [5, [6, 7], 8, 9], 10];
console.log(source.flat());
// [ 1, 2, 3, 4, 5, [ 6, 7 ], 8, 9, 10 ]

这个例子是个包含了三个维度(虽然不整齐)的数组,使用 ??flat()??? 减少了一个维度,其结果变成了两个维度。??flat()??? 可以通过参数指定展开的维度层数,这里只需要指定一个大于等于 ??2?? 的值,它就能把所有元素全部展平到一个一维数组中:
const source = [1, 2, [3, 4], [5, [6, 7], 8, 9], 10];
console.log(source.flat(10));
// [ 1, 2, 3, 4,5,6, 7, 8, 9, 10 ]

有了这个东西,我们在处理一些子项的时候就会比较方便。比如一个常见问题:
有一个二层的菜单数据,我想拿到所有菜单项列表,应该怎么办?数据如下
const data = https://www.songbingjia.com/android/[

label: "文件",
items: [
label: "打开", id: 11 ,
label: "保存", id: 12 ,
label: "关闭", id: 13
]
,

label: "帮助",
items: [
label: "查看帮助", id: 91 ,
label: "关于", id: 92
]

];
>

怎么办?毫无悬念应该是使用一个双层循环来处理。不过利用 ??map()??? 和 ??flat()??可以简化代码:
const allItems = data.map(( items ) => items).flat();
//^^^^^^^^^

第一步 ??map()??? 把 ?? label, items ??? 类型的元素映射成为 ??[...items]?? 这种形式的数组,映射结果是一个二维数组(示意):
[
[...文件菜单项],
[...帮助菜单项]
]

再用 ??flat()??? 展平,就得到了 ??[...文件菜单项, ...帮助菜单项]??,也就是预期的结果。
通常我们直接拿到二维数组来处理的情况极少,一般都需要先 ??map()??? 再 ??flat()???,所以 JavaScript 为这两个常用组合逻辑提供了 ??flatMap()??? 方法。要理解 ??flatMap()??? 的作用,就理解为先 ??map(...)??? 再 ??flat()?? 即可。上面的示例可改为
const allItems = data.flatMap(( items ) => items);
//^^^^^^^^

这里解决了一个两层结构的数据,如果是多层结构呢?多层结构不就是普通的树形结构,使用递归对所有子项进行 ??flatMap()?? 处理即可。代码先不提供,请读者动动脑。
四、过滤如果我们有一组数据,需要把其中符合某种条件的筛选出来使用,就会用到过滤,??filter()???。??filter()??? 接收一个用于判断的处理函数,并对每个元素使用该处理函数进行判断。如果该函数对某个元素的判断结果是真值,该元素会被保留;否则不会收录到结果中。??filter()?? 的结果是原数组的子集。
??filter()?? 的用法很好理解,比如下面这个示例筛选出能被 3 整除的数:
const a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const r = a.filter(n => n % 3 === 0);
// [ 3, 6, 9 ]

有两点需要强调:第一,如果所有元素都?不符合??条件,会得到一个空数组。既不是 ??null??? 也不是 ??undefined???,而是 ??[]??;
第二,如果所有元素都?符合?条件,会得到一个包含所有元素的?新??数组。它与原数组进行 ??===??? 或 ??==??? 比较均会得到 ??false??。
过滤虽然简单,但是要注意灵活运用。比如说需要统计某组数据中符合条件的个数,一般会想到遍历计数。但我们也可以先按指定条件过滤,再取结果数组的 ??length??。
五、查找查找和过滤的区别在于:查找是找到一个符合条件的元素即可,而过滤是找到全部。从实现效果上来说,??arr.filter(fn)[0]??? 就可以达到查找效果。只不过 ?

    推荐阅读