写「好」程序并不像是一本圣经,只要照着做就好,而是要不断审视自己的周遭,有没有什麽地方可以改进?用更有效率、更好读的写法?因此最好先从自己比较熟悉的东西开始。
Array 是从小陪伴各位长大的朋友(?),以我自己工作这几年,真的是一天都离不开 array,因为它序列化的特性,配合回圈来处理大量相似资料,是非常有效又实用的。
当然会看到这里的朋友,应该用 array 都用到像呼吸一样了(A之呼吸!),虽然大家语法都懂,但今天会帮大家介绍,各种实战上会如何使用?甚至是常见的组合技。
今天来看看以下这几个 array 常用的 method:
forEach
filter
map
reduce
最後会再来比较一下,forEach
一枝独秀 v.s. filter
/map
/reduce
联合军
基本语法(完整版参考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
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
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()
方法会建立一个经指定之函式运算後,由原阵列中通过该函式检验之元素所构成的新阵列。(参考 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()
方法会建立一个新的阵列,其内容为原阵列的每一个元素经由回呼函式运算後所回传的结果之集合。(参考 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,将一个任务分割成两个区块,大幅提升可读性。
这边私心介绍一下我常在使用的 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()
方法将一个累加器及阵列中每项元素(由左至右)传入回呼函式,将阵列化为单一值。(参考 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
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
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}
}
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 }
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
注意,联合军的版本,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 很丑,啊联合军也没比较简洁啊!
是的,如果论程序码长度来说,的确输了一截,但同时也发现,程序码从一整坨,被切成三段,分别处理了「筛选」、「转换」、「整合」,分工合作,清楚明确,以可读性来说,如果很清楚 filter
、map
、reduce
各自的用途,就很容易分段读懂整段 code。
但反过来说,当资料量愈庞大,这种「联合军」的写法会跑愈慢,因为比起 forEach
只跑一次回圈,联合军跑了三次,资料愈多差异愈大。
因此我认为这是一个 trade-off,需要根据团队习惯、效能或环境需求,自行判断要采用哪一种写法。如果资料量不大,其实很推荐使用联合军的写法,因为起码我要看懂别人写 forEach
,真的是需要花比较多时间QQ
我们非常熟悉的阵列,也有着许多平常没用过的写法,但每个 method 各有各自擅长的项目,在对的时间使用对的 method,在可读性与效能上取得平衡,是让程序码迈向更「好」的第一步!
天平的两端
藏着各自的风景
写着各自的故事
D15. 字元阵列(2) 前一篇有讲到字元的输出是printf("%c",a[i...
今天是动态规划的最後一天,整理几题比较复杂的动态规划题目,当作前面几天内容的总结~~直接进例题 例题...
前言 前几天介绍了 mutex, semaphore, spinlock, read-write l...
IDE介面左侧是专案区,主要编写程序码的maic.c也在其中,而下方Drivers/Src当中可以看...
在前一篇文章中,我们有提到如果要搜寻部分字元的话,可以使用 _ 以及 % 这两种 但是如果想要将 _...