Javascript陣列方法中,相比map
、filter
、forEach
等常用的迭代方法,reduce
常常被我們所忽略,今天一起來探究一下reduce
在我們實戰開發當中,能有哪些妙用之處,下面從reduce
語法開始介紹。
語法
array.reduce(function(accumulator, arrayElement, currentIndex, arr), initialValue)
若傳入初始值,accumulator首次迭代就是初始值,否則就是陣列的第一個元素;後續迭代中將是上一次迭代函式返回的結果。所以,假如陣列的長度為n,如果傳入初始值,迭代次數為n;否則為n-1。
比如實現陣列 arr = [1,2,3,4] 求陣列的和
let arr = [1,2,3,4];
arr.reduce(function(pre,cur){return pre + cur}); // return 10
實際上reduce還有很多重要的用法,這是因為累加器的值可以不必為簡單型別(如數字或字串),它也可以是結構化型別(如陣列或物件),這使得我們可以用它做一些其他有用的事情,比如:
- 將陣列轉換為物件
- 展開更大的陣列
- 在一次遍歷中進行兩次計算
- 將對映和過濾函式組合
- 按順序執行非同步函式
將陣列轉化為物件
在實際業務開發中,你可能遇到過這樣的情況,後臺介面返回的陣列型別,你需要將它轉化為一個根據id值作為key,將陣列每項作為value的物件進行查詢。
例如:
const userList = [
{
id: 1,
username: 'john',
sex: 1,
email: 'john@163.com'
},
{
id: 2,
username: 'jerry',
sex: 1,
email: 'jerry@163.com'
},
{
id: 3,
username: 'nancy',
sex: 0,
email: ''
}
];
如果你用過lodash這個庫,使用_.keyBy
這個方法就能進行轉換,但用reduce
也能實現這樣的需求。
function keyByUsernameReducer(acc, person) {
return {...acc, [person.id]: person};
}
const userObj = peopleArr.reduce(keyByUsernameReducer, {});
console.log(userObj);
將小陣列展開成大陣列
試想這樣一個場景,我們將一堆純文字行讀入陣列中,我們想用逗號分隔每一行,生成一個更大的陣列名單。
const fileLines = [
'Inspector Algar,Inspector Bardle,Mr. Barker,Inspector Barton',
'Inspector Baynes,Inspector Bradstreet,Inspector Sam Brown',
'Monsieur Dubugue,Birdy Edwards,Inspector Forbes,Inspector Forrester',
'Inspector Gregory,Inspector Tobias Gregson,Inspector Hill',
'Inspector Stanley Hopkins,Inspector Athelney Jones'
];
function splitLineReducer(acc, line) {
return acc.concat(line.split(/,/g));
}
const investigators = fileLines.reduce(splitLineReducer, []);
console.log(investigators);
// [
// "Inspector Algar",
// "Inspector Bardle",
// "Mr. Barker",
// "Inspector Barton",
// "Inspector Baynes",
// "Inspector Bradstreet",
// "Inspector Sam Brown",
// "Monsieur Dubugue",
// "Birdy Edwards",
// "Inspector Forbes",
// "Inspector Forrester",
// "Inspector Gregory",
// "Inspector Tobias Gregson",
// "Inspector Hill",
// "Inspector Stanley Hopkins",
// "Inspector Athelney Jones"
// ]
我們從長度為5的陣列開始,最後得到一個長度為16的陣列。
另一種常見增加陣列的情況是flatMap,有時候我們用map方法需要將二級陣列展開,這時可以用reduce實現扁平化
例如:
Array.prototype.flatMap = function(f) {
const reducer = (acc, item) => acc.concat(f(item));
return this.reduce(reducer, []);
}
const arr = ["今天天氣不錯", "", "早上好"]
const arr1 = arr.map(s => s.split(""))
// [["今", "天", "天", "氣", "不", "錯"],[""],["早", "上", "好"]]
const arr2 = arr.flatMap(s => s.split(''));
// ["今", "天", "天", "氣", "不", "錯", "", "早", "上", "好"]
在一次遍歷中進行兩次計算
有時我們需要對陣列進行兩次計算。例如,我們可能想要計算數字列表的最大值和最小值。我們可以通過兩次通過這樣做:
const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
const maxReading = readings.reduce((x, y) => Math.max(x, y), Number.MIN_VALUE);
const minReading = readings.reduce((x, y) => Math.min(x, y), Number.MAX_VALUE);
console.log({minReading, maxReading});
// {minReading: 0.2, maxReading: 5.5}
這需要遍歷我們的陣列兩次。但是,有時我們可能不想這樣做。因為.reduce()讓我們返回我們想要的任何型別,我們不必返回數字。我們可以將兩個值編碼到一個物件中。然後我們可以在每次迭代時進行兩次計算,並且只遍歷陣列一次:
const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
function minMaxReducer(acc, reading) {
return {
minReading: Math.min(acc.minReading, reading),
maxReading: Math.max(acc.maxReading, reading),
};
}
const initMinMax = {
minReading: Number.MAX_VALUE,
maxReading: Number.MIN_VALUE,
};
const minMax = readings.reduce(minMaxReducer, initMinMax);
console.log(minMax);
// {minReading: 0.2, maxReading: 5.5}
將對映和過濾合併為一個過程
還是先前那個使用者列表,我們希望找到沒有電子郵件地址的人的使用者名稱,返回它們使用者名稱用逗號拼接的字串。一種方法是使用兩個單獨的操作:
- 獲取過濾無電子郵件後的條目
- 獲取使用者名稱並拼接
將它們放在一起可能看起來像這樣:
function notEmptyEmail(x) {
return !!x.email
}
function notEmptyEmailUsername(a, b) {
return a ? `${a},${b.username}` : b.username
}
const userWithEmail = userList.filter(notEmptyEmail);
const userWithEmailFormatStr = userWithEmail.reduce(notEmptyEmailUsername, '');
console.log(userWithEmailFormatStr);
// 'john,jerry'
現在,這段程式碼是完全可讀的,對於小的樣本資料不會有效能問題,但是如果我們有一個龐大的陣列呢?如果我們修改我們的reducer回撥,那麼我們可以一次完成所有事情:
function notEmptyEmail(x) {
return !!x.email
}
function notEmptyEmailUsername(usernameAcc, person){
return (notEmptyEmail(person))
? (usernameAcc ? `${usernameAcc},${person.username}` : `${person.username}`) : usernameAcc;
}
const userWithEmailFormatStr = userList.reduce(notEmptyEmailUsername, '');
console.log(userWithEmailFormatStr);
// 'john,jerry'
在這個版本中,我們只遍歷一次陣列,一般建議使用filter
和map
的組合,除非發現效能問題,才推薦使用reduce
去做優化。
按順序執行非同步函式
我們可以做的另一件事.reduce()是按順序執行promises(而不是並行)。如果您對API請求有速率限制,或者您需要將每個prmise的結果傳遞到下一個promise,reduce
可以幫助到你。
舉一個例子,假設我們想要為userList
陣列中的每個人獲取訊息。
function fetchMessages(username) {
return fetch(`https://example.com/api/messages/${username}`)
.then(response => response.json());
}
function getUsername(person) {
return person.username;
}
async function chainedFetchMessages(p, username) {
// In this function, p is a promise. We wait for it to finish,
// then run fetchMessages().
const obj = await p;
const data = await fetchMessages(username);
return { ...obj, [username]: data};
}
const msgObj = userList
.map(getUsername)
.reduce(chainedFetchMessages, Promise.resolve({}))
.then(console.log);
// {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}
async
函式返回一個 Promise 物件,可以使用then
方法新增回撥函式。當函式執行的時候,一旦遇到await
就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
請注意,在此我們傳遞Promise作為初始值Promise.resolve()
,我們的第一個API呼叫將立即執行。
下面是不使用async
語法糖的版本
function fetchMessages(username) {
return fetch(`https://example.com/api/messages/${username}`)
.then(response => response.json());
}
function getUsername(person) {
return person.username;
}
function chainedFetchMessages(p, username) {
// In this function, p is a promise. We wait for it to finish,
// then run fetchMessages().
return p.then((obj)=>{
return fetchMessages(username).then(data=>{
return {
...obj,
[username]: data
}
})
})
}
const msgObj = peopleArr
.map(getUsername)
.reduce(chainedFetchMessages, Promise.resolve({}))
.then(console.log);
// {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}
PS:更多前端資訊、技術乾貨,請關注公眾號「前端新視界」