上手使用 JavaScript 的 Map、Reduce 吧!
雖然有些概念類似甚至可以相通,但這裡並不是指常聽到的「MapReduce」,本文目的不是要討論如何運用 MapReduce 這樣的架構去處理大資料庫。這裡真正要討論的是,如何使用 JavaScript 裡陣列(Array)中的 .map() 和 .reduce() 方法,並把一些常見的使用方法和情境描述出來大家進行參考。
很多人對這兩個方法不習慣,原因不外乎是這兩種方法本來就不是一個非常直覺的東西,在大多數 JavaScript 語言的開發情境中,其實也沒有非得使用的理由。但不得不說,習慣了這兩個對陣列操作的方法,程式碼會變得簡潔,也更容易能處理一整批的資料。有時也能順便學習到一些「Functional Programming」會用到的概念,無論是在改善程式品質,還是投資自己的角度上,都有相當好處。
從最簡單的遍歷陣列開始
面對一個陣列裡的一堆資料,我們一定是從遍歷開始,一一處理裡面的每一筆資料。你也許已經非常熟悉如何遍歷陣列,最常見的不外乎就是兩種做法。
使用 for-loop:
var myArr = [ 1, 2, 3 ];
for (var index in myArr) {
console.log(myArr[index]);
}
使用陣列內建的 forEach 方法:
var myArr = [ 1, 2, 3 ];
myArr.forEach(function(element) {
console.log(element);
});
使用 .map() 對每個陣列元素加工
有些時候,我們想對每個陣列元素(Element)進行加工處理,於是最土法煉鋼的方法大概就是這樣:
幫每個元素加一:
var myArr = [ 1, 2, 3 ];
for (var index in myArr) {
myArr[index] = myArr[index] + 1;
}
// [ 2, 3, 4 ]
console.log(myArr);
這時你可以使用 .map() 方法來達成同樣目的:
var myArr = [ 1, 2, 3 ];
var newArr = myArr.map(function(element) {
return element + 1;
});
// [ 2, 3, 4 ]
console.log(newArr);
.map() 會將每一個元素代入處理函數,而處理函數回傳的值,會被收集組成一個新的陣列,這個新的陣列元素數量會和原本陣列的一樣。換句話說,同樣是對陣列加工後得到結果,它會回傳一個新的、加工過後的陣列,而不會修改原本的陣列內容。
使用 .map() 進行資料校正處理
當我們了解 .map() 的運作原理後,可以使用它做到更多資料處理的事,例如資料的校正或過濾。
舉例來說,若是我們得到一個包含許多數值的陣列,而我們想限定這些數值不得超過我們設定的上限值,這時我們可以這樣處理,來得到一個經過檢查校正過後的資料結果:
var myArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
var newArr = myArr.map(function(element) {
// 數值大於五的數值視為五
if (element > 5)
return 5;
return element;
});
// [ 1, 2, 3, 4, 5, 5, 5, 5, 5, 5 ]
console.log(newArr);
使用 .reduce() 進行數值加總
處理陣列資料的工作中,其中一項最常見的就是數值加總,或是進行統計運算。同樣的,若你使用土法煉鋼的做法,大致上如下:
var myArr = [ 1, 2, 3 ];
var result = 0;
for (var index in myArr) {
result += myArr[index];
}
// 6
console.log(result);
若使用 .reduce(),可以這樣寫:
var myArr = [ 1, 2, 3 ];
// 處理每個元素後等待回傳結果,第一次處理時代入初始值 0
var result = myArr.reduce(function(prev, element) {
// 與之前的數值加總,回傳後代入下一輪的處理
return prev + element;
}, 0);
// 6
console.log(result);
我們可以看到,改用 .reduce() 之後,陣列元素的加總計算,不會再一直存取到外部的 result 變數,而是算完結果後才將結果統計結果回傳。這樣做的好處,是不會再跨 Scope 去存取外部的變數,這對 JavaScript 這種有複雜 Scope 設計的語言來說,程式碼不會到處去污染。
把 .map() 和 .reduce() 串接起來吧!
這兩種方法都是用來處理陣列,所以我們可以輕易地串接兩者,以前面的例子來說,可以先對陣列資料進行校正和加工,然後對資料進行收斂和加總:
var myArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
var result = myArr
.map(function(element) {
// 數值大於五的數值視為五
if (element > 5)
return 5;
return element;
})
.reduce(function(prev, element) {
// 與之前的數值加總,回傳後代入下一輪的處理
return prev + element;
}, 0);
// 40
console.log(result);
利用 .reduce() 進行陣列扁平化
如果你開始查 .reduce() 的資料,應該會看到一些 MDN 文件,會提到一些相當實用的功能,其中一個就是扁平化陣列的應用。簡單來說,就是將一個複雜的陣列,扁平化成一維,這在很多資料處理或數值計算上相當有用。
var myArr = [
[ 1, 2 ],
[ 3, 4, 5 ],
[ 6, 7, 8 ]
];
// 將所有元素都與之前代入的陣列相接起來,第一次處理時代入初始值空陣列
var newArr = myArr.reduce(function(arr, element) {
// ex: [ 1, 2 ].concat([ 3, 4, 5 ])
return arr.concat(element);
}, []);
// [ 1, 2, 3, 4, 5, 6, 7, 8 ]
console.log(newArr);
所以這個處理函數將會被執行三次:
- 將空陣列與 [ 1, 2 ] 相接起來後回傳
- 將被代入的 [ 1, 2 ] 與 [ 3, 4, 5 ] 相接起來後回傳
- 將被代入的 [ 1, 2, 3, 4, 5 ] 與 [ 6, 7, 8 ] 相接起來後回傳
利用 .reduce() 進行資料歸納和統計吧!
我們也可以利用 .reduce() 配合上物件操作,對陣列的內容進行統計工作:
var myArr = [
'C/C++',
'JavaScript',
'Ruby',
'Java',
'Objective-C',
'JavaScript',
'PHP'
];
// 計算出每種語言出現過幾次
var langStatistics = myArr.reduce(function(langs, langName) {
if (langs.hasOwnProperty(langName)) {
langs[langName]++
} else {
langs[langName] = 1;
}
return langs;
}, {});
// { 'C/C++': 1, 'JavaScript': 2, 'Ruby': 1, 'Java': 1, 'Objective-C': 1, 'PHP': 1 }
console.log(langStatistics);
如果想要處理的資料是 Object 的形式怎麼辦?
運用 Object.keys() 這樣的技巧,我們可以把 .map() 或 .reduce() 結合使用到 Object 的資料上使用,這樣就可以對 Object 資料進行相同的統計運算或數值計算。
var data = {
'Fred': 1,
'Leon': 2,
'Wesley': 3,
'Chuck': 4,
'Denny': 5
};
// 使用 Object.keys() 取得包含所有 key 的陣列
var result = Object.keys(data).reduce(function(prev, name) {
// 利用 key 取得原始物件中的值,然後加總
return data[name] + prev;
}, 0);
// 15
console.log(result);
你在寫啥?結合 ECMAScript 6 後,世界都不一樣了。
ES6 已經上了實際的戰場,當 .map()/.reduce() 方法加上箭頭函數(Arrow Function
),然後又配合上 JavaScript 語言的特性,整個程式碼將變得更為簡短乾淨。
let newArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map((value) => value + 1);
當箭頭函數只有一個參數時,可以省去括號「()」:
let newArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map(value => value + 1);
註:不過,對於不習慣的人來說,更難閱讀了。但在開放原始碼和社群的圈子裡,因為已經被大量使用,所以最好趕快習慣它,會方便你更容易看懂坊間的各種「新」程式碼。
後記
當然,濫用 map/reduce 也可能會造成程式碼難以閱讀,無論是哪一種程式的技巧,這肯定都是一個問題。但至於什麼時候該用,什麼時候不該用,並不在本文範疇,個人認為,我們得先熟練使用這兩種方法,用熟了,再接著探討「好的使用情境」才有意義。因為很多人不熟悉,又不敢亂用,就更沒有機會習慣它了。
所以,先別想太多,嘗試習慣使用它們吧!
留言
張貼留言