JavaScript Reduce使用詳解

學會這一個技巧 Reduce 讓你開啟編程新世界

Learning This Reduce Skill and a Whole New World Will Open up for You 🎉

reduce 可謂是 JS 數組方法最靈活的一個,因為可以替代數組的其他方法,比如 map / filter / some / every 等,也是最難理解的一個方法,lodash 很多方法也可以用其實現,學會 reduce 將給與開發者另一種函數式(Functional)、聲明式(Declarative)的視角解決問題,而不是以往的過程式(Procedual)或命令式(Imperative)

其中一個難點在於判斷 accaccumulation 的類型以及如何選擇初始值,其實有個小技巧,可以幫助我們找到合適的初始值,我們想要的返回值的類型和 acc 類型需要是一樣的,比如求和最終結果是數字,則 acc 應該是數字類型,故其初始化必定是 0

下面開始鞏固對 reduce 的理解和用法。

map

根據小技巧,map 最終返回值是數組,故 acc 也應該是一個數組,初始值使用空數組即可。

/**
 * 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

根據小技巧,filter 最終返回值也是數組,故 acc 也應該是一個數組,使用空數組即可。

/**
 * 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 當目標數組為空返回 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

附帶提醒,二者對結果沒影響但有性能區別,acc 放到前面因為是短路算法,可避免無謂的計算,故性能更高。

acc || predicate(val, idx, arr)

predicate(val, idx, arr) || acc

every

every 目標數組為空則返回 true,故初始值為 true

function every(arr, predicate) {
 return arr.reduce((acc, val, idx) => acc && predicate(val, idx, arr), true)
}

findIndex

findIndex 目標數組為空返回 -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

一、實現以下函數

/**
 * 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

利用該函數可以實現一些比較復雜的處理過程

// 挑選出 val 是正數的項對其 val 乘以 0.1 系數,然後將所有項的 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

二、實現以下函數,既能實現上述 pipe 的功能,而且返回函數接納參數個數可不定

/**
 * 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 已實現

/**
 * Sum up the numbers.
 * @param args number[]
 * @returns {number} the total sum.
 */
function sum(...args) {
 return args.reduce((a, b) => a + b);
}

參考答案

一、返回函數接受一個參數

省略過濾掉非函數的 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
  )
 }
}

二、返回函數接受不定參數

同樣省略瞭過濾掉非函數的 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) => {
  // 第一個已經處理,隻需處理剩餘的
  return funcs.slice(1).reduce(
   (acc, func) => func(acc),
   
   // 首先將特殊情況處理掉當做 `acc`
   funcs[0](...args)
  )
 }
}

第二種寫法的 funcs[0](...args) 這個坑要註意,數組為空就爆炸瞭,因為空指針瞭。

實現 lodash.get

實現 get 使得以下示例返回 'hello world'

const obj = { a: { b: { c: 'hello world' } } };

get(obj, 'a.b.c');

函數簽名:

/**
 * 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);
}

實現 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 隻能執行 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; }

過濾掉對象中的空值

實現

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 的枚舉

實現 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 串行執行器

利用 reduce 我們可以讓不定數量的 promises 串行執行,在實際項目中能發揮很大作用。此處不細講,請參考我的下一篇文章 JS 請求調度器。

拓展

請使用 jest 作為測試框架,給本文的所有方法書寫單測
更多習題見 github.com/you-dont-ne…

以上就是JavaScript Reduce使用詳解的詳細內容,更多關於JavaScript Reduce使用的資料請關註WalkonNet其它相關文章!

推薦閱讀: