乐趣区

关于javascript:JavaScript-数据处理-操作数组

程序中的罕用数据汇合无非两类,列表 (List) 和映射 (Map)。在 JavaScript 的语言根底中就提供了这两种汇合构造的反对 —— 用数组 (Array) 示意列表,用间接对象 (Plain Object) 示意映射(属性键值对映射)。

明天咱们只说数组。

Array 类中提供的实例办法能够看进去,数组涵盖了个别的列表操作,增删改查俱全,更提供了 shift()/unshift()push()/pop() 这样的办法,使数组具备队列和栈的基本功能。

除了日常的 CRUD 之外,最重要的就是对列表进行齐全或局部遍历,拿到预期的后果,这些遍历操作包含

  1. 逐个遍历:forforEach()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);

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 = [
 {
     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] 就能够达到查找成果。只不过 filter() 肯定会遍历残缺个数组。

而业余的 find() 则会在找到第一个符合条件的元素之后立刻终止遍历,节约工夫和计算资源。从后果上来说,find() 能够看作是 filter()[0] 的便捷实现(当然性能也更好),其参数(处理函数)和 filter() 雷同。

find() 的后果是找到的元素,或者在什么都没找到的状况下返回 undefined。所以在应用 find() 的时候肯定要留神其后果有可能是 undefined,应用前应该进行有效性判断。当然,如果联合可选链运算符 (?.) 和空值合并运算符 (??),也很容易参加表达式。

不过有时候,咱们查找一个元素并不是想应用它,而是想替换或者删除它。这时候拿到元素自身是很难办的,咱们更须要索引号。查 MDN 很容易就能查到 findIndex() 办法。它的用法和 find() 雷同,只是返回的是元素索引而不是元素自身。如果没有找到符合条件的元素,findIndex() 会返回 -1

说到 findIndex() 就很容易联想到 indexOf()indexOf() 的参数是一个值(或对象),它会在数组中去寻找这个值的地位并返回进去。对于根本类型的值来说,很好用。然而对于对象元素,就要小心了,看看上面这个例子

const m = {v: 1};
const a = [m, { v: 2}, {v: 3}];

console.log(a.indexOf(m));          // 0
console.log(a.indexOf({ v: 1}));   // -1

同样示意为 {v: 1},但他们真的不是同一个对象!

顺便提一提,有人喜爱用 arr.indexOf(v) >= 0 来判断数组中是否蕴含某个元素,其实无妨应用业余的 includes() 办法。includes() 间接返回一个布尔值,而且它容许通过第 2 个参数指定开始查找的地位。详见 MDN。

那么,如果想依据某个判断办法(函数)来判断数据中是否存在某个符合条件的元素,是不是要用 arr.find(fn) !== undefined 来判断呢?个别状况下是能够,但如果遇到非凡状况 ——

// 查找判断是否蕴含假值
const a = [undefined, 1, 2, 3];
const hasFalsy = a.find(it => !it) !== undefined;  // false

很可怜,这个后果是错的,肉眼可见,它的确蕴含假值!

按条件查找 是否存在 的正确做法是应用 some() 办法(之前提到过,忘了没?):

const hasFalsy = a.some(it => !it);

六、归约

归约是对 reduce 的直译,而 reduce() 也是数组的一个办法。

之所以须要归约,因为有时候咱们须要进行的解决并不会像后面提到的那么简略,比方一个常见的利用 —— 累加。想想看,使之前的解决形式,只能通过 for 或者 forEach() 循环累加。这两种形式都须要额定的长期变量,对函数式编程不太敌对。如果用 reduce(),大略是这样:

const a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const sum = a.reduce((sum, n) => sum + n);

再简单一点,如果想把数组中的奇偶数分离出来,别离放在两个数组中:

const a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const [even, odd] = a.reduce((acc, n) => {acc[n % 2].push(n);
        return acc;
    },
    [[], []]
);

和下面一段代码不同,这里应用 reduce() 时给了第二 个参数 [[], []]。这是一个初始值,会在第一次调用处理函数的时候作为第 1 个参数传入函数,也就是上例中的 acc 参数。

那么有心的读者会提出疑难,为什么第一个示例不须要初始值,那种状况下初始的 sum 参数是什么货色?

这里不得不说 reduce() 的两个特点:

  1. reduce() 每一次的处理结果,也就是处理函数的返回值,会作为下一次解决的第一个参数;
  2. 调用 reduce() 时如果给了第二个参数,即初始值,它会用作第一次解决时的第一个参数;但如果没给初始值,在第一次解决时,数组中的前两个元素会别离作为传入处理函数的前两个参数。

看到第 2 条,是不是又产生了一个疑难:如果数组里只有一个元素数呢?—— 那么处理函数会被疏忽,这个元素就是 reduce() 的后果。

那么……数组是空的会怎么?这个问题能把 reduce() 问哭 —— 我报错还不行吗?!

学会 reduce() 会发现后面提到的所有遍历过程都能够用 reduce() 来实现,毕竟它利用起来很灵便 —— 然而何必呢?何况,reduce() 也只能通过 throw 来中断 —— 当然不必放心 throw 中断拿不到后果,把后果作为 throw 的对象抛出来,里面不就拿到了吗?hiahiahia~~

七、截取

从数据中截取一部分,毫无疑问,当然是用 slice() 办法。该办法两个参数示意要截取的索引终点和起点,其中起点索引对应的元素不蕴含在内,用数学语言来说,这是一个左闭右开区间。须要留神的是终点必须小于起点才有可能取到元素,否则后果肯定是一个空数组。这示意,如果应用 arr.slice(-Infinity, Infinity) 能够取到所有有元素 —— 但谁会这么干呢,间接 arr.slice(0) 不香么(不给第二个参数示意始终截取到最初一个元素)?

另外,slice() 还有两个有意思的特点:

  • 不论是终点还是起点,如果给出了超出数组索引范畴的值,不会引起谬误,它会取数组索引范畴和指定范畴相交的局部;
  • 如果给的索引是正数,比方 -n,它会依照 arr.length - n 来计算索引。这样一来,想依据结尾地位来获取元素就变得容易了。

示例就不写了,slice() 文档说得很明确,不难理解。

有人要问,很多汇合的流式解决都会有 take()skip() 办法,用于截取指定地位肯定数量的元素,JavaScript 有吗?—— slice() 可不就是?它和 .skip().take() 根本等价。说 根本等价,是因为 take() 的参数示意的是一个长度,而 slice() 的第二个参数示意的是一个地位,所以(上面的 .skip().take() 是伪代码,仅示意):

  • arr.skip(m).take(n) 等价于 arr.slice(m, m + n)
  • arr.skip(m) 等价于 arr.slice(m)
  • arr.take(n) 等价于 arr.slice(0, n)

不过在应用 slice() 的同时,别忘了 JavaScript 的解构赋值语法也能够用于简略地截取数组。比如说

const a = [1, 2, 3, 4, 5, 6];
const [,, ...rest] = a;
// rest = [3, 4, 5, 6]

它和 a.slice(2) 的后果一样的,然而相比之外,slice() 语义更明确,怎么都比数逗号个数强吧。然而解构语法在某些状况下还是挺不便的,比方在 CLI 中拆分命令和参数的时候:

const args = "git config --list --global".split(/\s+/);
const [exec, cmd, ...params] = args;
// exec: "git"
// cmd: "config"
// params: ["--list", "--global"]

八、创立数组

尽管本文次要是讲基于遍历的数组操作,但既然都说到了 slice() 这个非遍历类的操作,无妨顺便再提一下创立数组。

  • 应用 [] 创立空数组,或已知大量元素的数组,最罕用
  • Array(n) 创立指定长度的数组,但留神这个数组尽管有长度,却 没有元素,也不能遍历。想让它有元素
  • Array.from(Array(n)),失去一个长度 n,所有元素都是 undefined 的数组;
  • [...Array(n)] 和下面一条后果一样;[...Array(n).values()] 也是一样;
  • Array(n).fill(1024),创立一个长度 n,元素均是 1024 的数组。当然也能够指定其余元素值;
  • Array.from 的第二个参数是个 mapper,所以 Array.from(Array(n), (_, i) => i) 能够创立一个元素是从 0n - 1 的数组;
  • [...Array(n).keys()] 能够创立和上一条一样的数组;

当初有一个问题,想创立一个 7×4 的二维数组,默认元素填 0,该怎么办?那还不简略,这样

const matrix = Array(4).fill(Array(7).fill(0));
// [//   [ 0, 0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0, 0]
// ]

仿佛没故障,来进行一个操作,看看成果如何?

matrix[0][4] = 4;
// [//   [ 0, 0, 0, 0, 4, 0, 0],
//   [0, 0, 0, 0, 4, 0, 0],
//   [0, 0, 0, 0, 4, 0, 0],
//   [0, 0, 0, 0, 4, 0, 0]
// ]

所有第二层数组索引为 4 的元素值都变成了 4 …… Why?

咱们把下面的初始化语句拆分一下,可能就明确了 ——

const row = Array(7).fill(0);
const matrix = Array(4).fill(row);

你看,这 4 行援用的都是一个数组,所以不论扭转哪个,输入来 4 行数据都会齐全一样(同一个数组能不一样吗)。

这是在初始化多维数组时最常见的坑。所以 Array(n).fill(v) 尽管好用,但肯定要审慎。这里如果应用带映射的 Array.from() 就没问题了:

const matrix = Array.from(Array(4),
    () => Array.from(Array(7), () => 0)
);

小结

因为 JavaScript 的动静个性,不须要定义一大堆的数据类型来示意不同的列表,就一个数组搞定。尽管还是有肯定的局限性,然而曾经能适应大部分利用场景了。本文次要介绍了数组的基本操作,更多更具体的内容能够参阅 MDN – Array 文档。下次我会再讲讲映射(对象)的基本操作,以及数组和对象之间的联结利用。对于 JavaScript 的数据处理,读者们也能够去理解一下 Lodash,提供了十分多的工具。

退出移动版