共计 5740 个字符,预计需要花费 15 分钟才能阅读完成。
JavaScript 实现数组更多的高阶函数
场景
虽说人人平等,但有些人更加平等。
为什么有了 Lodash 这种通用函数工具库,吾辈要写这篇文章呢?吾辈在 SegmentFault 上经常看到关于 JavaScript 数组的相关疑问,甚至于,相同类型的问题,只是数据变化了一些,就直接提出了一个新的问题(实际上,对自身并无帮助)。简单搜索了一下 Array,居然有 2360+ 条的结果,足可见这类问题的频率之高。若是有一篇适合 JavaScript 萌新阅读的自己实现数组更多操作的文章,情况是否会发生变化呢?
下面吾辈便来实现以下几种常见的操作
-
uniqueBy
: 去重 -
sortBy
: 排序 -
filterItems
: 过滤掉一些元素 -
diffBy
: 差异 -
groupBy
: 分组 - 递归操作
前言:
你至少需要了解 ES6 的一些特性你才能愉快的阅读
uniqueBy
: 去重
相关问题
- javascript 怎么实现多种数据类型的数组去重?
- JS 有没有比较高效的数组去重的方法?
/** | |
* 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
: 排序
相关问题
- js 中如何对含有特殊字符的数组进行排序?
- 以下数组怎么按名称排序
/** | |
* 快速根据指定函数对数组进行排序 | |
* 注: 使用递归实现,对于超大数组(其实前端的数组不可能特别大吧?# 笑)可能造成堆栈溢出 | |
* @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
: 差异
相关问题
- JS 求两个对象数组的差集
- JavaScript 数组系列问题:数组差集
/** | |
* 比较两个数组的差异 | |
* @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 数组分组?
/** | |
* 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
相关问题
- js 怎么把数组下面的对象里面的两个字段取出来组成一个新的对象,key:value 形式
/** | |
* 将数组映射为 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' => '崇文区'} |
递归
相关问题
- 复杂数组去重
- JavaScript 数组中包含数组如何去重?
以上种种操作皆是对一层数组进行操作,如果我们想对嵌套数组进行操作呢?例如上面这两个问题?其实问题是类似的,只是递归遍历数组而已。
/** | |
* 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。下面是两个例子,可以看一下能否发现什么问题
- js 怎么把数组下面的对象里面的两个字段取出来组成一个新的对象,key:value 形式
- JS 中处理 JSON 数据重复问题,取出里面 name 字段数值相同的作为一个数组;不相同的作为一个数组?
事实上,不管是问题还是答案,都没有突出核心 — Array 映射为 Map/Array 分组 ,而且这种问题和答案还层出不穷。如果对 Array 的 API 都没有看过一遍就来询问的话,对于帮助者来说却是太失礼了!
总结
JavaScript 对函数式编程支持很好,所以习惯高阶函数于我们而言是一件好事,将问题的本质抽离出来,而不是每次都局限于某个具体的问题上。
正文完
发表至: javascript
2019-07-06