2017-06-07
JavaScript
编程
活用原生方法解决数组问题
这几个月以来,我已经渐渐不用 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 结果如图
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’}]
或者是理所当然的,数组可以映射到由函数组成的数组。
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]

# 换个角度思考数组

我着重讨论了两个很重要的方法: mapfilter,它们都封装了 for 的过程,因此在调用的时候可以完成这样的两件事:
  1. 映射
  2. 筛选
如果把上面的过程看成是在数组上的 运算 ,这样来看数组具有一定的 集合性质
此外,大部分的数组问题,其实都可以归结为若干个上述的 子运算 的问题,利用函数把他们组装起来就可以得到整个问题的解。
比如对于下面的数组,它可以看成是一个集合,里面有 不同 的元素。
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 里的每一个 presonhead 都前置一个 http://

这些方法使得数组更像集合,并引入了代数理论的一些思想,使得可以数组和函数的层次上求解问题,因此我认为利用这些方法的目的其实是:提高抽象层次,更方便大脑的思考。
这种高层次思考的感觉可以看看我之前的对配置式表单验证的探索:
Vally - 配置式表单验证

# Links





回到顶部