JavaScript 實現數組更多的高階函數
場景
雖說人人平等,但有些人更加平等。
爲什麼有了 Lodash 這種通用函數工具庫,吾輩要寫這篇文章呢?吾輩在 SegmentFault 上經常看到關於 JavaScript 數組的相關疑問,甚至於,相同類型的問題,只是數據變化了一些,就直接提出了一個新的問題(實際上,對自身並無幫助)。簡單搜索了一下 Array,居然有 2360+ 條的結果,足可見這類問題的頻率之高。若是有一篇適合 JavaScript 萌新閱讀的自己實現數組更多操作的文章,情況是否會發生變化呢?
下面吾輩便來實現以下幾種常見的操作
-
uniqueBy
: 去重 -
sortBy
: 排序 -
filterItems
: 過濾掉一些元素 -
diffBy
: 差異 -
groupBy
: 分組 - 遞歸操作
前言:
你至少需要了解 ES6 的一些特性你才能愉快的閱讀
uniqueBy
: 去重
相關問題
/**
* js 的數組去重方法
* @param arr 要進行去重的數組
* @param kFn 唯一標識元素的方法,默認使用 {@link returnItself}
* @returns 進行去重操作之後得到的新的數組 (原數組並未改變)
*/
function uniqueBy(arr, kFn = val => val) {
const set = new Set()
return arr.filter((v, ...args) => {
const k = kFn(v, ...args)
if (set.has(k)) {
return false
}
set.add(k)
return true
})
}
使用
console.log(uniqueBy([1, 2, 3, '1', '2'])) // [ 1, 2, 3, '1', '2' ]
console.log(uniqueBy([1, 2, 3, '1', '2'], i => i + '')) // [ 1, 2, 3 ]
sortBy
: 排序
相關問題
/**
* 快速根據指定函數對數組進行排序
* 注: 使用遞歸實現,對於超大數組(其實前端的數組不可能特別大吧?#笑)可能造成堆棧溢出
* @param arr 需要排序的數組
* @param kFn 對數組中每個元素都產生可比較的值的函數,默認返回自身進行比較
* @returns 排序後的新數組
*/
function sortBy(arr, kFn = v => v) {
// TODO 此處爲了讓 typedoc 能生成文檔而不得不加上類型
const newArr = arr.map((v, i) => [v, i])
function _sort(arr, fn) {
// 邊界條件,如果傳入數組的值
if (arr.length <= 1) {
return arr
}
// 根據中間值對數組分治爲兩個數組
const medianIndex = Math.floor(arr.length / 2)
const medianValue = arr[medianIndex]
const left = []
const right = []
for (let i = 0, len = arr.length; i < len; i++) {
if (i === medianIndex) {
continue
}
const v = arr[i]
if (fn(v, medianValue) <= 0) {
left.push(v)
} else {
right.push(v)
}
}
return _sort(left, fn)
.concat([medianValue])
.concat(_sort(right, fn))
}
return _sort(newArr, ([t1, i1], [t2, i2]) => {
const k1 = kFn(t1, i1, arr)
const k2 = kFn(t2, i2, arr)
if (k1 === k2) {
return 0
} else if (k1 < k2) {
return -1
} else {
return 1
}
}).map(([_v, i]) => arr[i])
}
使用
console.log(sortBy([1, 3, 5, 2, 4])) // [ 1, 2, 3, 4, 5 ]
console.log(sortBy([1, 3, 5, '2', '4'])) // [ 1, '2', 3, '4', 5 ]
console.log(sortBy([1, 3, 5, '2', '4'], i => -i)) // [ 5, '4', 3, '2', 1 ]
filterItems
: 過濾掉一些元素
相關問題
/**
* 從數組中移除指定的元素
* 注: 時間複雜度爲 1~3On
* @param arr 需要被過濾的數組
* @param deleteItems 要過濾的元素數組
* @param kFn 每個元素的唯一鍵函數
*/
function filterItems(arr, deleteItems, kFn = v => v) {
const kSet = new Set(deleteItems.map(kFn))
return arr.filter((v, i, arr) => !kSet.has(kFn(v, i, arr)))
}
使用
console.log(filterItems([1, 2, 3, 4, 5], [1, 2, 0])) // [ 3, 4, 5 ]
console.log(filterItems([1, 2, 3, 4, 5], ['1', '2'], i => i + '')) // [ 3, 4, 5 ]
diffBy
: 差異
相關問題
/**
* 比較兩個數組的差異
* @param left 第一個數組
* @param right 第二個數組
* @param kFn 每個元素的唯一標識產生函數
* @returns 比較的差異結果
*/
function diffBy(left, right, kFn = v => v) {
// 首先得到兩個 kSet 集合用於過濾
const kThanSet = new Set(left.map(kFn))
const kThatSet = new Set(right.map(kFn))
const leftUnique = left.filter((v, ...args) => !kThatSet.has(kFn(v, ...args)))
const rightUnique = right.filter(
(v, ...args) => !kThanSet.has(kFn(v, ...args)),
)
const kLeftSet = new Set(leftUnique.map(kFn))
const common = left.filter((v, ...args) => !kLeftSet.has(kFn(v, ...args)))
return { left: leftUnique, right: rightUnique, common }
}
使用
console.log(diffBy([1, 2, 3], [2, 3, 4])) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4])) // { left: [ 1, 2 ], right: [ '2', 4 ], common: [ 3 ] }
console.log(diffBy([1, 2, 3], ['2', 3, 4], i => i + '')) // { left: [ 1 ], right: [ 4 ], common: [ 2, 3 ] }
groupBy
: 分組
相關問題
/**
* js 數組按照某個條件進行分組
*
* @param arr 要進行分組的數組
* @param kFn 元素分組的唯一標識函數
* @param vFn 元素分組的值處理的函數。第一個參數是累計值,第二個參數是當前正在迭代的元素,如果你使用過 {@link Array#reduce} 函數的話應該對此很熟悉
* @param init 每個分組的產生初始值的函數。類似於 reduce 的初始值,但它是一個函數,避免初始值在所有分組中進行累加。
* @returns 元素標識 => 數組映射 Map
*/
function groupBy(
arr,
kFn = v => v,
/**
* 默認的值處理函數
* @param res 最終 V 集合
* @param item 當前迭代的元素
* @returns 將當前元素合併後的最終 V 集合
*/
vFn = (res, item) => {
res.push(item)
return res
},
init = () => [],
) {
// 將元素按照分組條件進行分組得到一個 條件 -> 數組 的對象
return arr.reduce((res, item, index, arr) => {
const k = kFn(item, index, arr)
// 如果已經有這個鍵了就直接追加, 否則先將之初始化再追加元素
if (!res.has(k)) {
res.set(k, init())
}
res.set(k, vFn(res.get(k), item, index, arr))
return res
}, new Map())
}
使用
console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], i => i)) // Map { 1 => [ 1 ], 2 => [ 2, 2, 2 ], 4 => [ 4, 4 ], 5 => [ 5, 5 ], 6 => [ 6 ] }
console.log(groupBy([1, 2, 2, 2, 4, 4, 5, 5, 6], i => i % 2 === 0)) // Map { false => [ 1, 5, 5 ], true => [ 2, 2, 2, 4, 4, 6 ] }
console.log(
groupBy(
[1, 2, 2, 2, 4, 4, 5, 5, 6],
i => i % 2 === 0,
(res, i) => res.add(i),
() => new Set(),
),
) // Map { false => Set { 1, 5 }, true => Set { 2, 4, 6 } }
arrayToMap
: 轉換爲 Map
相關問題
/**
* 將數組映射爲 Map
* @param arr 數組
* @param k 產生 Map 元素唯一標識的函數,或者對象元素中的一個屬性名
* @param v 產生 Map 值的函數,默認爲返回數組的元素,或者對象元素中的一個屬性名
* @returns 映射產生的 map 集合
*/
export function arrayToMap(arr, k, v = val => val) {
const kFn = k instanceof Function ? k : item => Reflect.get(item, k)
const vFn = v instanceof Function ? v : item => Reflect.get(item, v)
return arr.reduce(
(res, item, index, arr) =>
res.set(kFn(item, index, arr), vFn(item, index, arr)),
new Map(),
)
}
使用
const county_list = [
{
id: 1,
code: '110101',
name: '東城區',
citycode: '110100',
},
{
id: 2,
code: '110102',
name: '西城區',
citycode: '110100',
},
{
id: 3,
code: '110103',
name: '崇文區',
citycode: '110100',
},
]
console.log(arrayToMap(county_list, 'code', 'name')) // Map { '110101' => '東城區', '110102' => '西城區', '110103' => '崇文區' }
console.log(arrayToMap(county_list, ({ code }) => code, ({ name }) => name)) // Map { '110101' => '東城區', '110102' => '西城區', '110103' => '崇文區' }
遞歸
相關問題
以上種種操作皆是對一層數組進行操作,如果我們想對嵌套數組進行操作呢?例如上面這兩個問題?其實問題是類似的,只是遞歸遍歷數組而已。
/**
* js 的數組遞歸去重方法
* @param arr 要進行去重的數組
* @param kFn 唯一標識元素的方法,默認使用 {@link returnItself},只對非數組元素生效
* @returns 進行去重操作之後得到的新的數組 (原數組並未改變)
*/
function deepUniqueBy(arr, kFn = val => val) {
const set = new Set()
return arr.reduce((res, v, i, arr) => {
if (Array.isArray(v)) {
res.push(deepUniqueBy(v))
return res
}
const k = kFn(v, i, arr)
if (!set.has(k)) {
set.add(k)
res.push(v)
}
return res
}, [])
}
使用
const testArr = [
1,
1,
3,
'hello',
[3, 4, 4, 'hello', '5', [5, 5, ['a', 'r']]],
{
key: 'test',
},
4,
[3, 0, 2, 3],
]
console.log(deepUniqueBy(testArr)) // [ 1, 3, 'hello', [ 3, 4, 'hello', '5', [ 5, [Object] ] ], { key: 'test' }, 4, [ 3, 0, 2 ] ]
反例
事實上,目前 SegmentFault 上存在着大量低質量且重複的問題及回答,關於這點確實比不上 StackOverflow。下面是兩個例子,可以看一下能否發現什麼問題
事實上,不管是問題還是答案,都沒有突出核心 -- Array 映射爲 Map/Array 分組,而且這種問題和答案還層出不窮。如果對 Array 的 API 都沒有看過一遍就來詢問的話,對於幫助者來說卻是太失禮了!
總結
JavaScript 對函數式編程支持很好,所以習慣高階函數於我們而言是一件好事,將問題的本質抽離出來,而不是每次都侷限於某個具體的問題上。