寫「好」程式並不像是一本聖經,只要照著做就好,而是要不斷審視自己的周遭,有沒有什麼地方可以改進?用更有效率、更好讀的寫法?因此最好先從自己比較熟悉的東西開始。
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,在可讀性與效能上取得平衡,是讓程式碼邁向更「好」的第一步!
天平的兩端
藏著各自的風景
寫著各自的故事
我是 reduce 派 XD
const totalPrice = arr.reduce((acc, item) => {
if((isUserVip || !item.vip) && item.price > 10000) {
acc = isUserVip
? acc + item.price * (1 - item.discount)
: acc + item.price
}
return acc
}, 0)
如果 forEach 裡面的處理邏輯變複雜的話,我想可以把邏輯拉出來,拆分、封裝成不同的 functions 之後再組合回去,也許可讀性會變好一點,譬如
const isValid = (item) => (isUserVip || !item.vip) && item.price > 10000
const sumPriceForVip = (item) => totalPrice += item.price * (1 - item.discount)
const sunPriceForNonVip = (item) => totalPrice += item.price
arr.forEach(item => {
if(isValid(item)) {
isUserVip ? sumPriceForVip(item) : sunPriceForNonVip(item)
}
});
(以上只是舉例,以這個簡單的 case 不用硬拆成這樣 :p)
OMG!我未來的某一個主題直接被你預測掉了XDDD 真的厲害
將 function 拆出來也是我滿喜歡做的事情(雖然我是命名苦手...),在提升共用性也有大加分!謝謝你幫忙舉例,驗證了程式優化絕對不只一條路~
ycchiuuuu 原來後面會提到 > < 只好加倍期待一下了哈哈
命名有時候真的很讓人困擾呢(這也會是未來的主題嗎 XD)
命名會不會獨立一個主題還不確定,但肯定是要花個半篇以上來講的,因為這也是我的痛點XD
動不動就會寫出 fetchCustomizedCategoriesIncludeOther
這種獵奇的 function 名字。。。
喜歡拆function,這樣寫測試也好寫,不會全部都綁在一起~~~
[問] JavaScript 的 functional programming 設施,是否有 lazy evaluation 機制?
嗨你好,可以透過 Lazy.js 或 immutable.js 等套件來輕鬆做到
因為 lazy evaluation 需要確保每一步驟都是 immutable,所以如果用我們熟知的原生寫法比較難辦到,引入套件來處理是一個比較方便的方式唷!