JavaScript Reduce使用詳解

學會這一個技巧 Reduce 讓你開啟編程新世界
Learning This Reduce Skill and a Whole New World Will Open up for You 🎉
reduce 可謂是 JS 數(shù)組方法最靈活的一個,因為可以替代數(shù)組的其他方法,比如 map / filter / some / every 等,也是最難理解的一個方法,lodash 很多方法也可以用其實現(xiàn),學會 reduce 將給與開發(fā)者另一種函數(shù)式(Functional)、聲明式(Declarative)的視角解決問題,而不是以往的過程式(Procedual)或命令式(Imperative)
其中一個難點在于判斷 acc 即 accumulation 的類型以及如何選擇初始值,其實有個小技巧,可以幫助我們找到合適的初始值,我們想要的返回值的類型和 acc 類型需要是一樣的,比如求和最終結(jié)果是數(shù)字,則 acc 應(yīng)該是數(shù)字類型,故其初始化必定是 0。
下面開始鞏固對 reduce 的理解和用法。
map
根據(jù)小技巧,map 最終返回值是數(shù)組,故 acc 也應(yīng)該是一個數(shù)組,初始值使用空數(shù)組即可。
/**
* Use `reduce` to implement the builtin `Array.prototype.map` method.
* @param {any[]} arr
* @param {(val: any, index: number, thisArray: any[]) => any} mapping
* @returns {any[]}
*/
function map(arr, mapping) {
return arr.reduce((acc, item, index) => [...acc, mapping(item, index, arr)], []);
}
測試
map([null, false, 1, 0, '', () => {}, NaN], val => !!val);
// [false, false, true, false, false, true, false]
filter
根據(jù)小技巧,filter 最終返回值也是數(shù)組,故 acc 也應(yīng)該是一個數(shù)組,使用空數(shù)組即可。
/**
* Use `reduce` to implement the builtin `Array.prototype.filter` method.
* @param {any[]} arr
* @param {(val: any, index: number, thisArray: any[]) => boolean} predicate
* @returns {any[]}
*/
function filter(arr, predicate) {
return arr.reduce((acc, item, index) => predicate(item, index, arr) ? [...acc, item] : acc, []);
}
測試
filter([null, false, 1, 0, '', () => {}, NaN], val => !!val);
// [1, () => {}]
some
some 當目標數(shù)組為空返回 false,故初始值為 false。
function some(arr, predicate) {
return arr.reduce((acc, val, idx) => acc || predicate(val, idx, arr), false)
}
測試:
some([null, false, 1, 0, '', () => {}, NaN], val => !!val);
// true
some([null, false, 0, '', NaN], val => !!val);
// false
附帶提醒,二者對結(jié)果沒影響但有性能區(qū)別,acc 放到前面因為是短路算法,可避免無謂的計算,故性能更高。
acc || predicate(val, idx, arr)
和
predicate(val, idx, arr) || acc
every
every 目標數(shù)組為空則返回 true,故初始值為 true
function every(arr, predicate) {
return arr.reduce((acc, val, idx) => acc && predicate(val, idx, arr), true)
}
findIndex
findIndex 目標數(shù)組為空返回 -1,故初始值 -1。
function findIndex(arr, predicate) {
const NOT_FOUND_INDEX = -1;
return arr.reduce((acc, val, idx) => {
if (acc === NOT_FOUND_INDEX) {
return predicate(val, idx, arr) ? idx : NOT_FOUND_INDEX;
}
return acc;
}, NOT_FOUND_INDEX)
}
測試
findIndex([5, 12, 8, 130, 44], (element) => element > 8) // 3
pipe
一、實現(xiàn)以下函數(shù)
/**
* Return a function to make the input value processed by the provided functions in sequence from left the right.
* @param {(funcs: any[]) => any} funcs
* @returns {(arg: any) => any}
*/
function pipe(...funcs) {}
使得
pipe(val => val * 2, Math.sqrt, val => val + 10)(2) // 12
利用該函數(shù)可以實現(xiàn)一些比較復(fù)雜的處理過程
// 挑選出 val 是正數(shù)的項對其 val 乘以 0.1 系數(shù),然后將所有項的 val 相加,最終得到 3
const process = pipe(
arr => arr.filter(({ val }) => val > 0),
arr => arr.map(item => ({ ...item, val: item.val * 0.1 })),
arr => arr.reduce((acc, { val }) => acc + val, 0)
);
process([{ val: -10 }, { val: 20 }, { val: -0.1 }, { val: 10 }]) // 3
二、實現(xiàn)以下函數(shù),既能實現(xiàn)上述 pipe 的功能,而且返回函數(shù)接納參數(shù)個數(shù)可不定
/**
* Return a function to make the input values processed by the provided functions in sequence from left the right.
* @param {(funcs: any[]) => any} funcs
* @returns {(args: any[]) => any}
*/
function pipe(...funcs) {}
使得以下單測通過
pipe(sum, Math.sqrt, val => val + 10)(0.1, 0.2, 0.7, 3) // 12
其中 sum 已實現(xiàn)
/**
* Sum up the numbers.
* @param args number[]
* @returns {number} the total sum.
*/
function sum(...args) {
return args.reduce((a, b) => a + b);
}
參考答案
一、返回函數(shù)接受一個參數(shù)
省略過濾掉非函數(shù)的 func 步驟
/**
* Return a function to make the input value processed by the provided functions in sequence from left the right.
* @param {(arg: any) => any} funcs
* @returns {(arg: any) => any}
*/
function pipe(...funcs) {
return (arg) => {
return funcs.reduce(
(acc, func) => func(acc),
arg
)
}
}
二、返回函數(shù)接受不定參數(shù)
同樣省略了過濾掉非函數(shù)的 func 步驟
/**
* Return a function to make the input value processed by the provided functions in sequence from left the right.
* @param {Array<(...args: any) => any>} funcs
* @returns {(...args: any[]) => any}
*/
function pipe(...funcs) {
// const realFuncs = funcs.filter(isFunction);
return (...args) => {
return funcs.reduce(
(acc, func, idx) => idx === 0 ? func(...acc) : func(acc),
args
)
}
}
性能更好的寫法,避免無謂的對比,浪費 CPU
function pipe(...funcs) {
return (...args) => {
// 第一個已經(jīng)處理,只需處理剩余的
return funcs.slice(1).reduce(
(acc, func) => func(acc),
// 首先將特殊情況處理掉當做 `acc`
funcs[0](...args)
)
}
}
第二種寫法的 funcs[0](...args) 這個坑要注意,數(shù)組為空就爆炸了,因為空指針了。
實現(xiàn) lodash.get
實現(xiàn) get 使得以下示例返回 'hello world'。
const obj = { a: { b: { c: 'hello world' } } };
get(obj, 'a.b.c');
函數(shù)簽名:
/**
* pluck the value by key path
* @param any object
* @param keyPath string 點分隔的 key 路徑
* @returns {any} 目標值
*/
function get(obj, keyPath) {}
參考答案
/**
* Pluck the value by key path.
* @param any object
* @param keyPath string 點分隔的 key 路徑
* @returns {any} 目標值
*/
function get(obj, keyPath) {
if (!obj) {
return undefined;
}
return keyPath.split('.').reduce((acc, key) => acc[key], obj);
}
實現(xiàn) lodash.flattenDeep
雖然使用 concat 和擴展運算符只能夠 flatten 一層,但通過遞歸可以去做到深度 flatten。
方法一:擴展運算符
function flatDeep(arr) {
return arr.reduce((acc, item) =>
Array.isArray(item) ? [...acc, ...flatDeep(item)] : [...acc, item],
[]
)
}
方法二:concat
function flatDeep(arr) {
return arr.reduce((acc, item) =>
acc.concat(Array.isArray(item) ? flatDeep(item) : item),
[]
)
}
有趣的性能對比,擴展操作符 7 萬次 1098ms,同樣的時間 concat 只能執(zhí)行 2 萬次
function flatDeep(arr) {
return arr.reduce((acc, item) =>
Array.isArray(item) ? [...acc, ...flatDeep(item)] : [...acc, item],
[]
)
}
var arr = repeat([1, [2], [[3]], [[[4]]]], 20);
console.log(arr);
console.log(flatDeep(arr));
console.time('concat')
for (i = 0; i < 7 * 10000; ++i) {
flatDeep(arr)
}
console.timeEnd('concat')
function repeat(arr, times) { let result = []; for (i = 0; i < times; ++i) { result.push(...arr) } return result; }
過濾掉對象中的空值
實現(xiàn)
clean({ foo: null, bar: undefined, baz: 'hello' })
// { baz: 'hello' }
答案
/**
* Filter out the `nil` (null or undefined) values.
* @param {object} obj
* @returns {any}
*
* @example clean({ foo: null, bar: undefined, baz: 'hello' })
*
* // => { baz: 'hello' }
*/
export function clean(obj) {
if (!obj) {
return obj;
}
return Object.keys(obj).reduce((acc, key) => {
if (!isNil(obj[key])) {
acc[key] = obj[key];
}
return acc;
}, {});
}
enumify
將常量對象模擬成 TS 的枚舉
實現(xiàn) enumify 使得
const Direction = {
UP: 0,
DOWN: 1,
LEFT: 2,
RIGHT: 3,
};
const actual = enumify(Direction);
const expected = {
UP: 0,
DOWN: 1,
LEFT: 2,
RIGHT: 3,
0: 'UP',
1: 'DOWN',
2: 'LEFT',
3: 'RIGHT',
};
deepStrictEqual(actual, expected);
答案:
/**
* Generate enum from object.
* @see https://www.typescriptlang.org/play?#code/KYOwrgtgBAglDeAoKUBOwAmUC8UCMANMmpgEw5SlEC+UiiAxgPYgDOTANsAHQdMDmAChjd0GAJQBuRi3ZdeA4QG08AXSmIgA
* @param {object} obj
* @returns {object}
*/
export function enumify(obj) {
if (!isPlainObject(obj)) {
throw new TypeError('the enumify target must be a plain object');
}
return Object.keys(obj).reduce((acc, key) => {
acc[key] = obj[key];
acc[obj[key]] = key;
return acc;
}, {});
}
Promise 串行執(zhí)行器
利用 reduce 我們可以讓不定數(shù)量的 promises 串行執(zhí)行,在實際項目中能發(fā)揮很大作用。此處不細講,請參考我的下一篇文章 JS 請求調(diào)度器。
拓展
請使用 jest 作為測試框架,給本文的所有方法書寫單測
更多習題見 github.com/you-dont-ne…
以上就是JavaScript Reduce使用詳解的詳細內(nèi)容,更多關(guān)于JavaScript Reduce使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解析JavaScript中 querySelector 與 getElementById 方法的區(qū)別
這篇文章主要介紹了JavaScript中 querySelector 與 getElementById 方法的區(qū)別,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10
JS實現(xiàn)的另類手風琴效果網(wǎng)頁內(nèi)容切換代碼
這篇文章主要介紹了JS實現(xiàn)的另類手風琴效果網(wǎng)頁內(nèi)容切換代碼,通過JavaScript響應(yīng)鼠標事件動態(tài)操作頁面元素樣式屬性實現(xiàn)手風琴效果,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09

