difference
_.difference()、 _.differenceBy()、 _.differenceWith()
为何将_.difference()
、_.differenceBy()
、_.differenceWith()
三个方法放在一起分析呢?因为它们的内部都是基于basedifference()
方法进行处理,只不过是传入baseDifference()
的参数不同罢了。
下面是difference.js、differenceBy.js、differenceWith.js文件的内容:
// difference.js
function difference(array, ...values) {
// 判断array是否是个类数组对象,如果是,则调用baseDifference方法处理,如果不是,则返回空数组
// 在调用baseDifference处理前,通过调用baseFatten方法将传入的values扁平化处理,关于baseFlatten扁平化处理的方法可见baseFlatten源码解析
// 从baseFlatten传入的参数可知:扁平化处理的层级为1,遍历values数组中的元素时每次都会调用isArrayLikeObject方法判断元素是否是类数组对象,且是严格模式下扁平化处理
// baseDifference方法的比较可见baseDifference源码解析
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))
: []
}
// differenceBy.js
function differenceBy(array, ...values) {
// 取values数组中的最后一个元素为迭代器iteratee
let iteratee = last(values)
// 如果迭代器iteratee是个类数组对象,则迭代器iteratee为undefined
if (isArrayLikeObject(iteratee)) {
iteratee = undefined
}
// 如果array是类数组,则调用baseDifference方法,baseDifference参数为array、扁平化处理的values以及迭代器iteratee函数,否则返回空数组
// 这块可能会有个疑问,就是如果values数组的最后一个元素为迭代器iteratee函数,那么在扁平化的时候是不是把这个函数也扁平化进去?答案是no,baseFlatten中第三个参数就是帮我们将非类数组类型的元素过滤掉
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), iteratee)
: []
}
// differenceWidth.js
function differenceWith(array, ...values) {
// 取values数组的最后一个元素为比较器comparator
let comparator = last(values)
// 如果比较器comparator是非数组类型,则比较器comparator为undefined
if (isArrayLikeObject(comparator)) {
comparator = undefined
}
// 如果array为类数组对象,则调用baseDifference方法,baseDifference的参数依次为array、扁平化处理的values数组、undefined、比较器comparator,否则返回空数组
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator)
: []
}
从上面的代码中可以看出当array为类数组时,调用baseDifference方法,那么baseDifference方法是什么呢?
// baseDifference.js
// baseDifference方法用来将array数组与values数组进行对比,将存在于两者之中的元素从array数组中剔除掉,array中剩余的值组成一个新数组返回
// 对比的时候可以传入迭代器iteratee函数,array和values数组中的每个元素都会调用迭代器iteratee进行处理,然后chubaseDifference对比处理后的值
// 对比的时候也可以传入比较器函数,在对比的时候调用comparator来比较array和values中每个元素,可以理解为比较器comparator定义了对比的规则,默认是看两个值是否相等
// baseDifference方法会接收4个参数,依次为需要处理的array数组、用来对比的values数组、迭代器iteratee、比较器comparator
// 迭代器iteratee是个function,array和values中的每个元素都需要调用该方法处理
function baseDifference(array, values, iteratee, comparator) {
// includes用于储存判断数组中是否存在某个值的方法
let includes = arrayIncludes
// isCommon用于区分是普通对比,还是特殊对比
let isCommon = true
const result = []
const valuesLength = values.length
// array为空直接返回空数组
if (!array.length) {
return result
}
// 如果iteratee存在,则遍历values并对其中每个元素都调用iteratee方法
if (iteratee) {
values = map(values, (value) => iteratee(value))
}
// 如果存在比较器comparator,则为特殊对比,includes为arrayIncludesWith,其中arrayIncludesWith方法中就可以传入比较器comparator
if (comparator) {
includes = arrayIncludesWith
isCommon = false
}
else if (values.length >= LARGE_ARRAY_SIZE) {
// LARGE_ARRAY_SIZE是个常量,值为200,values长度超过200,则为特殊处理,includes为cacheHas
includes = cacheHas
isCommon = false
values = new SetCache(values)
}
// 遍历array数组,
outer:
for (let value of array) {
// 如果存在iteratee,用iteratee处理value
const computed = iteratee == null ? value : iteratee(value)
value = (comparator || value !== 0) ? value : 0
// 如果isCommon为true且computed === computed时遍历values,判断values中的元素是否有与computed相同的,如果没有则将当前value添加result中
if (isCommon && computed === computed) {
let valuesIndex = valuesLength
// 遍历values,当values中有元素与computed相同时,跳出当前array的循环,继续进行array的下一个循环,这样可以减少不必要的循环
// 只有当遍历完values中所有的元素后,如果都没有与computed相同的,说明当前value是array独有的,那么将value添加到result中
while (valuesIndex--) {
if (values[valuesIndex] === computed) {
continue outer
}
}
result.push(value)
}
else if (!includes(values, computed, comparator)) {
// isCommon为false的条件是存在comparator或者values.length超过200,此时会调用includes进行判断
// 如果includes返回的结果为false,则表明在比较器comparator的规则下,values中包含computed,此时需将value添加到result中
result.push(value)
}
}
// 返回结果result
return result
}
实现思路:
difference、differenceBy、differenceWith方法都可以将一个数组(array)与多个数组(array1、array2、array3 …)进行对比,以剔除其中共同存在的元素,在与多个数组(array1、array2、array3 …)对比的时候,利用数组扁平化处理,将多个数组(array1、array2、array3 …)整合成一个数组(values),然后进行比较;
比较的时候,根据传入参数的不同采用不同的比较方法,对于difference方法,在对比array数组和整合数组values时,采用双层循环对比的一种方法,在外层遍历array数组,内部遍历values数组,判断values是否存在array数组中的元素,对于values中不存在array数组中的元素,则添加到新数组中返回,其中在双层循环时采用了标签语法,就是源码中的outer和continue outer语句,这样写的好处就是当values数组中存在array中的元素时就跳出当前循环,继续array的下一次循环,减少不必要的变量;
对于differenceBy方法,它的实现跟difference相似,不同的地方在于:difference在对比array和values数组时是对比数组中的原始数据,而differenceBy则是对array和values数组中的每个元素调用迭加器函数进行处理,然后对比处理后的值;
对于differenceWith方法,与前两种有些不太一样,前两种都是默认比较array和values中元素是否相同,而differenceWith通过传入一个比较器来自定义比较规则,differenceWith也是基于双层循环,外层循环array数组,内层遍历values数组,内层遍历时不再是简单的对比values中值与array的元素是否相同,而是在比较器的规则下将values中的值与array中的值对比,比较器返回true的表明array中该值需要剔除,否则会添加到新数组返回;
看完上面的长篇文字后,可以通过几个例子来看一下结果:
example:
_.difference([1, 3, 5], [1, [2, 3]], [4, 6])
// [1, [2, 3]], [4, 6]扁平化处理后得到[1, [2, 3], 4, 6],对比后结果为[3, 5]
// [3, 5]
_.difference([1, [2, 3], 5], [1, [2, 3]], [4, 6])
// [1, [2, 3]], [4, 6]扁平化处理后得到[1, [2, 3], 4, 6],由于两个数组中的[2, 3]不是同一个引用,所以对比后结果为[[2, 3], 5]
// [[2, 3], 5]
_.differenceBy([1.1, 3.6, 5.2], [1.6, 2.3, 3.7], Math.floor)
// 两个数组中每个元素经过Math.floor处理后变成[1, 3, 5]和[1, 2, 3],最终对比结果为[5.2]
// [5.2]
function customCompare(a, b) {
return a - b === 1
}
_.differenceWith([1, 3, 5], [1, 4, 3], customCompare)
// [1, 3, 5]中的每个元素与[1, 4, 3]比较,只有相差1的时候customCompare返回true,即不需要添加到新数组中返回,可以看出[1, 3, 5]中只有5与[1, 4, 3]中的4比较时差1,最终对比结果为[1, 3]
// [1, 3]
其实对于difference()、differenceBy()、differenceWith()方法的源码解析,里面涉及到一些其他方法,如:isArrayLikeObject、baseFlatten、arrayIncludes、arrayIncludesWith、cacheHas等等方法,我们在这儿没有详细展开,仅仅简单的介绍了它们的作用,因为我们主要分析的是difference实现的逻辑,其他的在这儿就不是重点了(看源码,主次要分明),不过这些方法会在后面需要的时候再进一步详细介绍;
遗留一个问题,就是上面baseDifference方法中,有个computed === computed的判断,一直不明白为什么,难道还会有computed与computed不等的情况?
2019/6/19
相关阅读
题目链接 题意 定义 f(y,k)f(y,k)f(y,k) 为 yyy 各个数位上数字的 kkk 次方和,问有多少个不同的 yyy 满足 x=f(y,k)−yx=f(y,k)-y