若是以为没有面试题,那么lodash每个方法就能够看成一个题目,能够看着效果反过来实现,以不一样的方法实现、多种方法实现,巩固基础。除了某些一瞬间就能够实现的函数,下面抽取部分函数做为试炼。时代在进步,下文全部的解法都采用es2015+前端
本文实现方法都是看效果倒推实现方法,并进行一些拓展和思考,和源码无关。lodash这个库在这里更像一个题库,给咱们刷题的es6
能收获什么:面试
概念:编程
注意:api
lodash数组方法里面有好几个函数是自己+By+With一组的。假设lodash里面有一个函数foo
,对应的有fooBy
、fooWith
方法。fooBy
、fooWith
方法多了一个参数,是对数据进行预处理的。fooBy
最后一个参数能够是函数、数组、字符串,若是是函数,则对前面数组参数每个元素进行预处理再执行真正的逻辑;若是是数组、字符串,则先调用_.property(lastArg)
返回一个函数,使用该函数对前面数组参数每个元素进行预处理数组
// 都是同样的
fooBy(arr, 'x');
fooWith(arr, _.property('x'));
fooWith(arr, item => item.x);
复制代码
下文有_.property
的简单实现,详细的等待后续更新ide
_.difference(array, [values])
,建立一个差别化后的数组_.difference([3, 2, 1], [4, 2]);
// => [3, 1]
复制代码
_.difference([3, 2, 1], [4, 2]);
// => [3, 1]
复制代码
function difference(origin, diff = []) {
return origin.reduce(
(acc, cur) => [...acc, ...(!diff.includes(cur) ? [cur] : [])],
[]
);
}
复制代码
_.differenceBy(array, [values], [iteratee=_.identity])
,这个方法相似 _.difference,除了它接受一个 iteratee 调用每个数组和值。iteratee 会传入一个参数:(value)。// example
_.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);
// => [3.1, 1.3]
// 使用了 `_.property` (返回给定对象的 path 的值的函数)的回调结果
_.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
// => [{ 'x': 2 }]
复制代码
function differenceBy(origin, diff = [], iteratee) {
return origin.reduce(
(acc, cur) => [...acc, ...(!~diff.findIndex(d => iteratee(d) === iteratee(cur)) ? [cur] : [])],
[]
);
}
// 注意,使用`[].find`找到假值会致使误判
复制代码
_.property(path)
建立一个返回给定对象的 path 的值的函数。参数: path (Array|string), 要获得值的属性路径。differenceBy第三个参数实际上也使用了 _.property
,下面实现differenceBy完整版// example
var objects = [
{ 'a': { 'b': { 'c': 2 } } },
{ 'a': { 'b': { 'c': 1 } } }
];
_.map(objects, _.property('a.b.c'));
// => [2, 1]
_.map(_.sortBy(objects, _.property(['a', 'b', 'c'])), 'a.b.c');
// => [1, 2]
复制代码
// 本文只实现一个简单的_.property
function property(path) {
return function(o) {
const temp = Array.isArray(path) ? path : path.split(".");
let res = o;
while (temp.length && res) {
res = res[temp.shift()];
}
return res;
};
}
function differenceBy(origin, diff = [], iteratee) {
iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
return origin.reduce(
(acc, cur) => [...acc, ...(!~diff.findIndex(d => iteratee(d) === iteratee(cur)) ? [cur] : [])],
[]
);
}
复制代码
_.intersection([arrays])
,建立一个包含全部使用 SameValueZero 进行等值比较后筛选的惟一值数组。// example
_.intersection([2, 1], [4, 2], [1, 2]);
// => [2]
复制代码
// 若是下一个组没有上一个组的元素,那么那个元素能够pass了
// 用当前组的结果和上一组对比,再将当前组的结果覆盖上一组的结果
// 使用一样的方法遍历每一组,最终结果便是交集
// 常规数组解法
function intersection(...arrs) {
let temp;
return arrs.reduce((acc, arr) => {
if (!acc) { // 第一次进来,第一组作参照物
return temp = arr
}
// 展转计算上一组结果
return temp = arr.reduce((ret, item) => {
// 上一组有本元素,且最终输出结果内没有本元素(保证去重)
if (temp.includes(item)) {
// 这里if不能合在一块儿使用&&,防止走到下面逻辑
if (!ret.includes(item)) {
ret.push(item);
}
} else {
// 发现本元素不在上一组的元素中,那就删掉
const idx = ret.findIndex(x => x === item)
if (~idx) {
ret.splice(idx, 1);
}
}
return ret
}, [])
}, void 0)
}
// 对象映射解法
function intersection(...arrs) {
let temp;
// 若是用Object.keys会转字符串因此使用Object.values
return Object.values(arrs.reduce((acc, arr) => {
if (!acc) {
// 第一次初始化麻烦一些
return temp = arr.reduce((o, c) => ({ ...o, [c]: c }), {})
}
return temp = arr.reduce((ret, item) => {
// 中间这里查找有没有存在, 不存在就从最终结果删除
// 必定要in不能ret[item] 判断,否则若是ret[item] 是假值就骗过这个if了
if (item in temp) {
ret[item] = item;
} else if (ret[item]) {
delete ret[item]; // in与delete配合美滋滋
}
return ret
}, {})
}, void 0))
}
// 高级解法
// Set内部使用SameValueZero加去重,无需担忧NaN, -0
// Set增长删除元素很方便,并且性能好
// 查询是否存在某元素只须要O(1)时间
function intersection(...arrs) {
let temp;
return [...arrs.reduce((acc, arr) => {
return temp = !acc ? new Set(arr) : arr.reduce((ret, item) => {
ret[temp.has(item) ? 'add' : 'delete'](item)
return ret
}, new Set);
}, void 0)]
}
复制代码
相似前面的differenceBy和differenceWith方法,基于上面的intersection方法修改,加上前面也说到了_.property的实现。intersectionBy除了它接受一个 iteratee 调用每个数组和值。iteratee 会传入一个参数:(value)。函数式编程
// example
_.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
// => [2.1]
// 使用了 `_.property` 的回调结果
_.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }]
复制代码
可是也不是很简单,前面的方案会致使_.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor)
返回的是2.4,因此须要改一下函数
// property继续copy前面的
function property(path) {
return function (o) {
const temp = Array.isArray(path) ? path : path.split(".");
let res = o;
while (temp.length && res) {
res = res[temp.shift()];
}
return res;
};
}
// sameValueZero标准
function sameValueZero(a, b) {
return (a === b) || (isNaN(a) && isNaN(b));
}
function intersectionBy(...arrs) {
let iteratee = arrs.pop();
iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
let temp;
return Object.values(arrs.reduce((acc, arr) => {
if (!acc) {
return temp = arr.reduce((o, c) => ({ ...o, [c]: c }), {})
}
return temp = arr.reduce((ret, item) => {
// 须要根据当前值iteratee一下,再从以前的值iteratee事后里面找
const prefix = iteratee(item);
const newTemp = Object.values(temp);
const newRet = Object.values(ret);
const compare = t => sameValueZero(prefix, iteratee(t));
if (newTemp.map(iteratee).includes(prefix)) {
const preKey = newTemp.find(compare);
if (preKey) {
ret[preKey] = preKey;
}
} else if (newRet.map(iteratee).includes(prefix)) {
const preKey = newRet.find(compare);
if (preKey) {
delete ret[preKey];
}
}
return ret;
}, {})
}, void 0))
}
复制代码
最开始使用的方法,是以当前的结果去覆盖前面的结果,因此致使intersectionBy
最终结果取的是后面组的元素。所以,若是想实现lodash的intersectionBy
,就要固定最开始的那一组,而后围绕那一组开始走后面的逻辑。再次实现intersectionBy
:性能
先实现普通的intersection
function intersection(...arrs) {
return [...arrs.reduce((acc, arr) => {
// 遍历set结构而不是遍历每一组数组
return !acc ? new Set(arr) : (acc.forEach(item => {
if (!arr.includes(item)) {
acc.delete(item)
}
}), acc)
}, void 0)]
}
复制代码
再稍微修改一下,实现intersectionBy,此时实现
intersectionBy
的难度是★
function intersectionBy(...arrs) {
let iteratee = arrs.pop();
iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
return [...arrs.reduce((acc, arr) => {
return !acc ? new Set(arr) : (acc.forEach(item => {
// set结构转化为iteratee每个元素后再对比
if (!arr.map(iteratee).includes(iteratee(item))) {
acc.delete(item)
}
}), acc)
}, void 0)]
}
复制代码
// example
_.union([2, 1], [4, 2], [1, 2]);
// => [2, 1, 4]
复制代码
// 方法1
function union(...arrs) {
return [...new Set(arrs.reduce((acc, arr) => [...acc, ...arr], []))]
}
// 方法2
function union(...arrs) {
return arrs.reduce((acc, arr) =>
[...acc, ...arr.filter(item => !acc.includes(item))], []);
}
复制代码
_.unionBy([arrays], [iteratee=_.identity])
。这个方法相似 _.union,除了它接受一个 iteratee 调用每个数组和值。也是和上面的by、with同样的_.property
已经实现,能够从上面copy)// example
_.unionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
// => [2.1, 1.2, 4.3]
// 使用了 `_.property` 的回调结果
_.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }, { 'x': 2 }]
复制代码
咱们在union的第二种方法基础上改,很快就能够实现
function unionBy(...arrs) {
let iteratee = arrs.pop();
iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
return arrs.reduce((acc, arr) =>
[...acc, ...arr.filter(item => !acc.map(iteratee).includes(iteratee(item)))], []);
}
复制代码
相似的就是
uniq
系列了,即数组去重,uniq
只传入一个数组进行去重。union
传入多个数组求并集,实际上就是合并全部的数组再去重,道理都是差很少的。有对象组成的数组那种完全去重,其实是_.uniqWith(arr, _.isEqual)
,_.isEqual
是一个深度对比相等的方法,后续详细展开
_.xor([arrays])
建立一个包含了全部惟一值的数组。使用了 symmetric difference 等值比较。// example
_.xor([2, 1], [4, 2]);
// => [1, 4]
复制代码
// 方法1
function xor(...arrs) {
return [...arrs.reduce((acc, arr) =>
!acc ? new Set(arr) : arr.reduce((res, item) =>
(res[res.has(item) ? 'delete' : 'add'](item), res)
, acc)
, void 0)]
}
// 方法2,比较容易扩展成By和With方法
function sameValueZero(a, b) {
return a === b || (isNaN(a) && isNaN(b));
}
function xor(...arrs) {
return arrs.reduce((acc, arr) => {
arr.forEach(item => {
const index = acc.findIndex(x => sameValueZero(x, item))
if (!~index) {
acc.push(item)
} else {
acc.splice(index, 1);
}
})
return acc;
}, [])
}
复制代码
ok,到了这里,
xorBy
和xorWith
你们都知道怎么作了吧
_.sortedIndex(array, value)
使用二进制的方式(二分查找)检索来决定 value 应该插入在数组中位置。它的 index 应该尽量的小以保证数组的排序。// example
_.sortedIndex([30, 50], 40);
// => 1
_.sortedIndex([4, 5], 4);
// => 0
复制代码
// 二分查找
function sortedIndex(arr, value) {
let low = 0
let high = arr == null ? low : arr.length
while (low < high) {
const mid = (low + high) >> 1
const medium = arr[mid]
if (medium !== null &&
medium < value) {
low = mid + 1
} else {
high = mid
}
}
return high
}
复制代码
sortedIndexBy也是一样的道理,就很少说了。相对的,还有sortedLastIndex方法,只是它是反过来遍历的:使用二进制的方式(二分查找)检索来决定 value 应该插入在数组中位置。它的 index 应该尽量的大以保证数组的排序。
关注公众号《不同的前端》,以不同的视角学习前端,快速成长,一块儿把玩最新的技术、探索各类黑科技