點燈坊

失くすものさえない今が強くなるチャンスよ

如何對 Array 內 Object 的所有的 Property 進行 Filter ?

Sam Xiao's Avatar 2019-08-16

實務上為了簡化 User 操作,我們希望輸入 Keyword 後,能對 Array 內 Object 所有 Property 都進行檢查,只要包含此 Keyword,就會顯示該 Object,這該如何實現呢 ?

Version

macOS Mojave 10.14.5
VS Code 1.37.0
Quokka 1.0.240
ECMAScript 2017
Ramda 0.26.1

Functional

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

// fn :: String -> [a] -> [a]
let fn = keyword => arr => arr.filter(
  x => x.title.includes(keyword.trim()) || 
  x.price.toString().includes(keyword.trim())
);

fn('FP')(data); // ?

data 內的 object 只有 titleprice 兩個 property,若 titleprice 內任何一個 property 包含 keyword,則顯示該 object。

建立 fn(),argument 為 keywordarr,直覺會使用 Array.prototype.filter() 處理。

x.titlex.price 取得資料,透過 String.prototype.includes() 判斷是否包含 keyword,若有則回傳 true,否則回傳 false,因為只要任何一個 property 成立皆可,所以使用 || 串起來。

由於 price 為 number,必須先經過 toString() 轉成 string 後才能使用 includes()

為了讓 空白 顯示所有資料,特別使用 keyword.trim() 將前後空白都清除。

filter000

Refactoring

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

// fn :: String -> [a] -> [a]
let fn = keyword => arr => arr.filter(x => Object.values(x).join(' ').includes(keyword.trim()));

fn('FP')(data); // ?

之前方法雖然可行,但缺點是 object 有幾個 property,|| 就要寫幾次,是否有更通用的方式能檢查 無限 property 呢 ?

若能將所有 property 取出轉成 string,只要 includes() 判斷 string 即可。

  • 使用 ES2017 的 Object.values() 只取 object 的 property value 部分成為 array
  • 再使用 Array.prototype.join() 將 array 轉成 string
  • 最後使用 String.prototype.includes() 判斷 keyword 是否存在

如此無論 object 有多少 property,寫法都不用改變。

filter001

Ramda

import { filter, values, join, includes, trim, compose } from 'ramda';

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

let pred = keyword => compose(
  includes(keyword),
  join(' '),
  values
);

// fn :: String -> [a] -> [a]
let fn = keyword => filter(pred(trim(keyword)));

fn('FP')(data); // ?

fn() 目前尚有 keywordarr 兩個 argument,我們嘗試使用 Ramda 的 filter()arr point-free。

filter() 的 predicate 也有 keywordx 兩個 argument,也嘗試將其 point-free。

其實由 Object.values(x).join(' ').includes() 已經看出端倪,就是先執行 Object.values(),再執行 join(),最後執行 includes(),以 FP 角度就是將 values()join()includes() 組合出新的 pred()

filter002

Point-free

import { filter, values, join, includes, trim, compose, useWith, identity } from 'ramda';

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
];

let pred = useWith(
  includes, [
    identity,
    compose(join(' '), values)
  ]
);

// fn :: String -> [a] -> [a]
let fn = useWith(
  filter, [
    compose(pred, trim),
    identity
  ]
);

fn('FP')(data); // ?

可否連 keyword 也 point-free 呢 ?

其實原本 fn()pred() 都是兩個 argument,經觀察得知:

  • fn() 最後執行為 filter(),需要 transformer function 運算後才將結果傳給 filter(),因此適合使用 useWith() point-free
  • pred() 最後執行為 includes(),需要 transformer function 運算後才將結果傳給 includes(),也適合使用 useWith() point-free

filter002

Conclusion

  • FP 可先從 ECMAScript 原生的 Array.prototypeString.prototype 學習,只是寫法沒那麼漂亮而已,但觀念都是一樣的
  • 接下來可嘗試將 data 使用 Ramda 加以 point-free
  • 若要連其他 argument 也 point-free,則要使用 useWith()converge()chain() …等高級 function,若覺得有難度,可先練習到能將 data 部分 point-free 即可,不用強求連其他 argument 也要 point-free,這需要時間練習

Reference

MDN, Array.prototype.filter()
MDN, String.prototype.includes()
MDN, String.prototype.trim()
MDN, Array.prototype.join()
MDN, Number.prototype.toString()
MDN, Object.values()
Ramda, filter()
Ramda, includes()
Ramda, trim()
Ramda, join()
Ramda, values()
Ramda, compose()
Ramda, useWith()
Ramda, identity()