彻底掌握Array(一)

914 阅读7分钟

各种 Method 的实战

今天来看看以下这几个 array 常用的 method:

  • forEach
  • filter
  • map
  • reduce

最后会再来比较一下,forEach一枝独秀 vs filtermap/reduce联合军

✅ forEach

基本语法 (完整版参考MDN )

array.forEach((element, index) => {
  // iterator
});

基本上就是 for 回圈的好读版本,其实 for 跟 forEach 能够做到的事情基本上一样,只是 for 比较像是基础设施,可以适用各种 case,而 forEach 更像是主题式乐园,可以符合大部分的使用情况。

因此,如果某些情况用 forEach 让你觉得有点卡,不妨试着改用 for 回圈,基本上回圈类能处理的都包办了!

forEach是 for 回圈的好读版本

forEach预设会把整个回圈每个项目都跑一次,而这也是大部分对于 array 跑回圈的需求,比起 for 回圈,少了一些 let i 或者 i++ 之类的指令,让整体可读性提升

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];
arr.forEach((element, index) => {
  console.log(element.name);
});

执行结果

TV
washing machine
laptop

✔ Continue

forEach没办法使用 continue 控制逻辑,只能透过 return 做到类似 continue 的效果:

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];
arr.forEach((element, index) => {
  if (element.price < 10000) {
      return;
  }
  // 只会印出大于 10000 元的东西
  console.log(element.name);
});

执行结果

TV
laptop

✔ Break

forEach也没办法使用break,类似效果可以用 flag 记录回圈的状态,但并不是真的「中断」后续回圈,只能「忽视」后续回圈:

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];
let breakFlag = false;

arr.forEach((element, index) => {
  if (breakFlag) {
      return;
  }
  
  if (element.price > 10000) {
      breakFlag = true;
  }
  // 只会印出第一个大于 10000 元的东西
  console.log(element.name);
});
执行结果

TV

✅ filter

filter()方法会建立一个经指定之函式运算后,由原阵列中通过该函式检验之元素所构成的新阵列。(参考MDN )

arr.filter(callback(element => {}))

✔ 过滤掉缺漏的元素

有时候并不是 array 里面每个元素都那么完整,可能会缺几个 property,而如果我们不慎存取到这些有缺漏的元素 property,就很容易产生 bug。

比如要将每个商品内的价格打八折 (但不是每个商品内都有price):

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine' },
    { id: 'item3', name: 'laptop', price: 25000 }
];

arr.forEach(item => {
    const discountPrice = item.price * 0.8;
    console.log(`${item.name}${discountPrice}`);
});

执行结果

TV10800
washing machine:NaN
laptop:20000

可以加上 filter 筛选:

    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine' },
    { id: 'item3', name: 'laptop', price: 25000 }
];

arr.filter(item => Number.isFinite(item.price))
   .forEach(item => {
        const discountPrice = item.price * 0.8;
        console.log(`${item.name}${discountPrice}`);
    });
    
执行结果
TV10800
laptop:20000

⚠注意第 7 行 如果没使用 Number.isFinite() 会怎么样?
arr.filter(item => item.price)
=>
可能会导致 price 是 0 的这种情况也被筛选掉,因为 0 转成 Boolean 会是 false。
虽然这个 case,0 打八折也是 0,不影响最后结果,但如果程式写起来跟自己的意图不一致,就是 buggy 的程式码,容易在未来意想不到的时候被回马枪。。。

✅ map

map()方法会建立一个新的阵列,其内容为原阵列的每一个元素经由回呼函式运算后所回传的结果之集合。(参考MDN )

arr.map(callback(element => {}))

✔ 显示购物清单在画面上

Array of Object 不像字串,没有办法直接显示在画面上,所以可以透过 map 来进行「转换」。

这边再多引用一个 join() 的方法,用来将 array 转成一个字串,详细用法可以参考MDN

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 }
];
const displayArr = arr.map(item => item.name).join('、');

console.log(`购买项目: ${displayArr}`);
执行结果

购买项目: TV、washing machine、laptop

filter跟 map 两个 method 是我个人常搭配一起使用的组合,因为 filter 会回传经过筛选的阵列,因此可以接着使用 map 等其它 array method,将一个任务分割成两个区块,大幅提升可读性

✅ reduce

reduce()方法将一个累加器及阵列中每项元素(由左至右)传入回呼函式,将阵列化为单一值。(参考MDN )

arr.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)

reduce的重点在于,把整个阵列的资料,透过「累积」产生一个最终的结果,所以只要感觉阵列中的元素,前一个要跟后一个有「互动」的,最后会产生单一个结果的,就可以考虑reduce

✔ 加总整个阵列

// 加总一般 Number
const arr = [0, 1, 2, 3, 4];
const sum = arr.reduce((prev, curr) => prev + curr, 0);
console.log(sum);

执行结果

10

✔ 加总 Array of Object

// 加总 Array of Object
const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];
const sum = arr.reduce((prev, curr) => prev + curr.price, 0);
console.log(sum);
执行结果

46700

✔ Array 转换成 Object (类似 groupby 功能)

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];
const resultObject = arr.reduce((prev, curr) => {
    prev[curr.id] = curr;
    return prev;
}, {});
console.log(resultObject);

执行结果

{
   item1: {id: "item1", name: "TV", price: 13500},
   item2: {id: "item2", name: "washing machine", price: 8200},
   item3: {id: "item3", name: "laptop", price: 25000}
}

✔ 统计 Array 重复元素数量

const arr = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
const resultObject = arr.reduce((prev, curr) => {
  if (curr in prev) {
    prev[curr]++;
  }
  else {
    prev[curr] = 1;
  }
  return prev;
}, {});
console.log(resultObject);

执行结果

{ Alice: 2, Bob: 1, Tiff: 1, Bruce: 1 }

forEach 一枝独秀 vs filter/map/reduce 联合军

filter用途在于「筛选
map用途在于「转换
reduce用途在于「整合

有趣的是,forEach其实可以一个函式就完成上面三个的功能。

比如要计算超过 10000 元商品的总价:

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];

// 联合军(filter + map + reduce)
const totalPrice = arr.filter(item => item.price > 10000)
                      .map(item => item.price)
                      .reduce((prev, curr) => prev + curr, 0);
                      
// forEach 的版本
let totalPrice = 0;
arr.forEach(item => {
    if(item.price > 10000) {
        totalPrice += item.price;
    }
});

// 以上两个 totalPrice 都是 38500

效能 vs 可读性

注意,联合军的版本,arr 阵列其实跑了 3 次回圈 (不过 map 跟 reduce 的回圈比较小一点就是了),而 forEach 只跑了 1 次回圈。

❓ 看起来 forEach 光靠一个 function 就搞定,甚至连效率都比联合军高,那干嘛还要有其它 method 啊?都给 forEach 玩就好了啊?

原因是,上面的例子只是为了快速理解两边的差异,告诉大家其实 forEach 可以做的事情跟联合军一样,但真实在职场上,你可能会遇到类似这样的东西,不妨试试,能不能短时间看懂这段逻辑:

const arr = [
    { id: 'item1', name: 'TV', price: 13500, vip: false, discount: 0.15 },
    { id: 'item2', name: 'washing machine', price: 8200, vip: false, discount: 0.1 },
    { id: 'item3', name: 'laptop', price: 25000, vip: false, discount: 0.12 },
    { id: 'item4', name: 'vip product', price: 99999, vip: true, discount: 0.3 },
];
const isUserVip = true;
                      
// forEach 的版本
let totalPrice = 0;
arr.forEach(item => {
    if((isUserVip || !item.vip) && item.price > 10000) {
        if(isUserVip) {
            totalPrice += item.price * (1 - item.discount);
        } else {
            totalPrice += item.price;
        }
    }
});

// 联合军(filter + map + reduce)
const totalPrice = arr
.filter(item => (isUserVip || !item.vip) && item.price > 10000)
.map(item => {
    if(isUserVip) {
        return item.price * (1 - item.discount);
    } else {
        return item.price;
    }
})
.reduce((prev, curr) => {
    return prev + curr;
}, 0);

上面这段 code 的目的是:「根据 user 是否为 vip,算出其身分可购买且大于 10000 元的所有商品各自打折后的总价」

虽然 forEach 的 code 很丑,啊联合军也没比较简洁啊!

Trade-off

是的,如果论程式码长度来说,的确输了一截,但同时也发现,程式码从一整坨,被切成三段,分别处理了「筛选」、「转换」、「整合」,分工合作,清楚明确,以可读性来说,如果很清楚filtermapreduce各自的用途,就很容易分段读懂整段 code。

但反过来说,当资料量愈庞大,这种「联合军」的写法会跑愈慢,因为比起 forEach 只跑一次回圈,联合军跑了三次,资料愈多差异愈大。

因此我认为这是一个 trade-off,需要根据团队习惯、效能或环境需求,自行判断要采用哪一种写法。如果资料量不大,其实很推荐使用联合军的写法,因为起码我要看懂别人写forEach,真的是需要花比较多时间QQ

结语

Array还有下一期,下期见!

参考资料

Array MDN