这几个月以来,我已经渐渐不用
for
来处理数组问题了。 原因主要还是 for
容易造成心智负担,而利用原生方法的 forEach
则可以让我不那么关注遍历过程,而把精力放在处理数组元素上,顺便偷偷懒 ~~这种方法里面最最常用的是
forEach
map
filter
reduce
而使用这些方法
函数式
地处理数组问题,会有很大的收获。# 从 forEach 说起 ↵
数组的
forEach
做的事情很简单 ————遍历数组,执行函数
下面的代码可以实现一个
forEach
:(它可以实现跟数组的 Array.prototype.forEach
完全一样的执行结果)00var forEach = (cb, arr) => { 01 for (let i = 0; i < arr.length; i++){ 02 cb(arr[i], i, arr); 03 } 04} 05 06forEach((item, index, itself) => { 07 console.log(item, index, itself); 08}, ["hello", "Array's", "forEach"]); 09 10// => 11// hello 0 ["hello", "Array's", "forEach"] 12// Array's 1 ["hello", "Array's", "forEach"] 13// forEach 2 ["hello", "Array's", "forEach"]
可以看到这里把
for
这个过程封装了,我们只需要关注 forEach
所需要的第一个作为参数的 函数
就可以了。数组上的
Array.prototype.forEach
用起来比上面要舒服点,没那么啰嗦。00['A', 'B', 'C'].forEach((item, idx, its) => { 01 console.log(item, idx, its); 02}); 03// => 04// A 0 ['A', 'B', 'C', 'D'] 05// B 1 ['A', 'B', 'C', 'D'] 06// C 2 ['A', 'B', 'C', 'D']
结果如图

forEach 结果如图
# map filter reduce ↵
map
很方便的指明了数组的集合性质,认为数组可以 映射 map
到另外一个数组上,这点很神 ———— 遍历一次数组,把 item index itself 应用到第一个作为参数的函数里,并让返回值作为新的数组元素。这样讲可能有些晦涩,因为我认为理解它的最好方式就是去实现它、阅读它的一个实现:
00var map = (cb, arr) => { 01 // 新数组 02 let newArr = []; 03 for (let i = 0; i < arr.length; i++){ 04 // 这里意味着赋值 05 newArr[i] = cb(arr[i], i, arr); 06 } 07 08 return newArr; 09} 10// 以上是实现 下面是调用 11 12var a = [5, 6, 7, 8]; 13var res = map(function(item, idx, its){ 14 return item + 1; 15}, a); 16 17console.log(res); 18// => 19// [6, 7, 8, 9]
它能解决很多很多的数组问题,比如为数组每一个元素
+1
则仅需使用以下代码就可以实现: (这里使用了原生方法,没有使用自己的实现)00var a = [5, 6, 7, 8]; 01a.map(function(item, idx, its){ 02 return item + 1; 03}); 04 05// => 06// [6, 7, 8, 9]
利用箭头函数,我们可以更偷懒的编程:
00a.map(e => e + 1)
从上面的实现可以看到:数组可以映射为任何可以被
比如:
return
的值构成的数组,比如:
[1, 2, 3, 4] ===> [‘A’, ‘B’, ‘C’, ‘D’]
[‘A’, ‘B’, ‘C’, ‘D’] ===> [{c: ‘A’}, {c: ‘B’}, {c: ‘C’}, {c: ‘D’}]
[‘A’, ‘B’, ‘C’, ‘D’] ===> [{c: ‘A’}, {c: ‘B’}, {c: ‘C’}, {c: ‘D’}]
或者是理所当然的,数组可以映射到由函数组成的数组。
00var keys = ['A', 'B', 'C', 'D']; 01 02var says = [0, 1, 2, 3].map(e => { 03 return () => console.log(keys[e]); 04}).forEach(fn => fn()); 05// => 06// A 07// B 08// C 09// D
filter 则多少有些 map 的意味,他会根据返回值
筛选
元素,如果返回 true
则保留,如果返回 false
则删除,当然它也很容易实现:00var filter = (cb, arr) => { 01 let res = []; 02 for (let i = 0; i < arr.length; i++){ 03 if (!!cb(arr[i], i, arr) === true){ 04 // 如果返回值是 true 05 // 则 push 进一个数组 06 res.push(arr[i]); 07 } 08 } 09 10 return res; 11} 12// 以下是一次调用 filter 的例子 13 14// 我只要大于等于3的值们 15var a = [1, 2, 3, 4, 5]; 16var res = filter((item, index, itself) => { 17 // 如果确实大于等于3 则返回true 即保留它 18 // 否则,返回fasle 将不会保留 19 return item >= 3; 20}, a); 21 22console.log(res); 23// => 24// [3, 4, 5]
结合原生和利用箭头函数可以写成更短:
00a.filter(e => e >= 3);
如果要我在
forEach
map
filter
reduce
里选一个最喜欢的,我会毫不犹豫地选择 reduce
,因为 reduce
可以很优雅地实现其余的三个。以下是
reduce
的一个实现 (跟原生数组的实现有些不同)00var reduce = (cb, arr, first) => { 01 // acc 是 accmulate(积累) 的缩写 02 let i, acc; 03 // 如果存在 first 作为初始值 04 if (first){ 05 acc = first; 06 } else { // 如果不存在 first 作为初始值 07 acc = arr[0]; 08 // 从 1 开始 reduce 09 i = 1; 10 } 11 12 for (; i < arr.length; i++){ 13 // 每次 cb 的返回值 作为下一次的 acc 14 acc = cb(acc, arr[i], i, arr); 15 } 16 17 return acc; 18} 19// 以下是调用 数组求和 20var a = [5, 6, 7]; 21// cur 代表 current 当前位置 22var sum = reduce((acc, cur, idx, its) => { 23 return acc + cur; 24}, a, 0); // 指定第一次的 acc 是 0 25 26console.log(sum); 27// => 28// 18
上述的过程用原生写法就是: (尽量简洁)
00var sum = a.reduce((acc, cur) => { 01 return acc + cur 02}, 0); // 第一次调用函数的时候的 acc 是 0
上面的
sum
较好的说明了问题: reduce
意为 减少
,通常的语义是这样一个意思: 把数组归约为另外一个值
,这个值不仅仅可以是数字,也可以是字符串、数组和对象等,所以你完全可以把一个数组 reduce
为另外一个数组,这样就可以实现 map
filter
了:(用 reduce 实现 map )00 01var mapByReduce = (cb, arr) => { 02 return arr.reduce((acc, cur, idx, its) => { 03 let newVal = cb(cur, idx, its); 04 acc.push(newVal); 05 06 return acc; 07 }, []); // 指定初始acc为空数组 08} 09// 以下是调用 a 的每一个元素变成原来的 2 倍 10var a = [1, 2, 3, 4]; 11 12var res = mapByReduce(e => e * 2, a); 13 14console.log(res); 15// => 16// [2, 4, 6, 8]
# 换个角度思考数组 ↵
我着重讨论了两个很重要的方法:
map
和 filter
,它们都封装了 for
的过程,因此在调用的时候可以完成这样的两件事:- 映射
- 筛选
如果把上面的过程看成是在数组上的
运算
,这样来看数组具有一定的 集合性质
此外,大部分的数组问题,其实都可以归结为若干个上述的
子运算
的问题,利用函数把他们组装起来就可以得到整个问题的解。比如对于下面的数组,它可以看成是一个集合,里面有
不同
的元素。00var presons = [ 01 { 02 head: 'test.com/photo/0.jpg' 03 }, { 04 head: 'test.com/photo/1.jpg' 05 }, { 06 head: 'test.com/photo/2.jpg' 07 } 08];
如果单纯的用
head
作为图片 src
是不可用的,应该给它们加上 http://
这样的协议名才可以正确地作为图片的 src
使用,如果用 for
来做,也不是什么难题,但是因此要写 for 要处理 i,也要处理结果增加了不必要的负担,而用 map
来做可以让代码一目了然,而且一行就可以实现:00presons.map(person => 'http://' + person.head);
意思很显然:
persons
里的每一个 preson
的 head
都前置一个 http://
这些方法使得数组更像集合,并引入了代数理论的一些思想,使得可以数组和函数的层次上求解问题,因此我认为利用这些方法的目的其实是:提高抽象层次,更方便大脑的思考。
这种高层次思考的感觉可以看看我之前的对配置式表单验证的探索:
Vally - 配置式表单验证
Vally - 配置式表单验证