Day 2 - Array 阵列组合技 (1)

前言

写「好」程序并不像是一本圣经,只要照着做就好,而是要不断审视自己的周遭,有没有什麽地方可以改进?用更有效率、更好读的写法?因此最好先从自己比较熟悉的东西开始。

Array 是从小陪伴各位长大的朋友(?),以我自己工作这几年,真的是一天都离不开 array,因为它序列化的特性,配合回圈来处理大量相似资料,是非常有效又实用的。

当然会看到这里的朋友,应该用 array 都用到像呼吸一样了(A之呼吸!),虽然大家语法都懂,但今天会帮大家介绍,各种实战上会如何使用?甚至是常见的组合技。

各种 Method 的实战

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

  • forEach
  • filter
  • map
  • reduce

最後会再来比较一下,forEach 一枝独秀 v.s. filter/map/reduce 联合军

✅ forEach

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

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

基本上就是 for 回圈的好读版本,其实 forforEach 能够做到的事情基本上一样,只是 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}`);
});

执行结果

TV:10800
washing machine:NaN
laptop:20000

可以加上 filter 筛选:

const arr = [
    { 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}`);
    });

执行结果

TV:10800
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

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

✔ React jsx render HTML

这边私心介绍一下我常在使用的 React,因为有 jsx,可以在 js 里面写类似 HTML 的语法并输出,而这时候就可以搭配 filter & map,把存在 js 的阵列经过 filter 筛选之後,在经过 map 转换成 HTML 输出:

const arr = [
    { id: 'item1', name: 'TV', price: 13500, vip: false },
    { id: 'item2', name: 'washing machine', price: 8200, vip: false },
    { id: 'item3', name: 'laptop', price: 25000, vip: false },
    { id: 'item4', name: 'vip product', price: 99999, vip: true },
];
const isUserVip = false;

// ...

return arr
        .filter(item => isUserVip || !item.vip)
        .map(item => (
            <div key={item.id} >
                <div>{item.name}</div>
                <div>{item.price}</div>
            </div>
        ));

✅ 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 一枝独秀 v.s. 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

效能 v.s. 可读性

注意,联合军的版本,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

结语

我们非常熟悉的阵列,也有着许多平常没用过的写法,但每个 method 各有各自擅长的项目,在对的时间使用对的 method,在可读性与效能上取得平衡,是让程序码迈向更「好」的第一步!

天平的两端
藏着各自的风景
写着各自的故事

参考资料

Array MDN


<<:  [Day 2]如何买、到哪买?

>>:  Day 5 - 原型 (4): 帖子页元件

D15. 学习基础C、C++语言

D15. 字元阵列(2) 前一篇有讲到字元的输出是printf("%c",a[i...

DAY21-动态规划(四)

今天是动态规划的最後一天,整理几题比较复杂的动态规划题目,当作前面几天内容的总结~~直接进例题 例题...

Day25 RCU 同步机制

前言 前几天介绍了 mutex, semaphore, spinlock, read-write l...

2. STM32-STM32CubeIDE 介面导览/编译

IDE介面左侧是专案区,主要编写程序码的maic.c也在其中,而下方Drivers/Src当中可以看...

16.MYSQL搜寻特殊字元

在前一篇文章中,我们有提到如果要搜寻部分字元的话,可以使用 _ 以及 % 这两种 但是如果想要将 _...