0%

lodash源码解析:difference、differenceBy、differenceWith、last、isArrayLike、isObjectLike、isArrayLike、isLength、isArguments

由于difference系列引用的方法过多,所以在之前的文章《lodash源码解析:baseDifference、map》中把公用的私有方法baseDifference单独进行了分析,本篇继续顺着分析difference系列方法。

引用的私有方法

baseDifference

difference系列方法的核心基础方法,见前一篇文章《lodash源码解析:baseDifference、map

getTag

返回目标的tag(比如[object Null]),见之前文章《lodash源码解析:chunk、slice、toInteger、toFinite、toNumber、isObject、isSymbol

isFlattenable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import isArguments from '../isArguments.js'

/** 内置值引用 */
// 内置的Symbol.isConcatSpreadable符号用于配置某对象作为Array.prototype.concat()方法的参数时是否展开其数组元素。
const spreadableSymbol = Symbol.isConcatSpreadable

/**
* 检查value是否是一个可扁平化的arguments对象或数组
*
* @private
* @param {*} value 要检查的值
* @returns {boolean} 如果要检查的值是可扁平化的则返回true,否则返回false
*/
function isFlattenable(value) {
// 是数组则返回true
// 是arguments对象则返回true
// Symbol.isConcatSpreadable属性为true则返回true
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable
return Array.isArray(value) || isArguments(value) ||
!!(value && value[spreadableSymbol])
}

export default isFlattenable

baseFlatten

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import isFlattenable from './isFlattenable.js'

/**
* `flatten`方法的基本方法,支持带约束条件的扁平化
*
* @private
* @param {Array} array 要扁平化的数组
* @param {number} depth 最大的递归深度
* @param {boolean} [predicate=isFlattenable] 每次迭代调用的函数
* @param {boolean} [isStrict] 仅限通过`predicate`检查的值。
* @param {Array} [result=[]] 初始的结果数组
* @returns {Array} 返回一个新的扁平化后的数组。
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
// 给predicate和result置初始值
predicate || (predicate = isFlattenable)
result || (result = [])

// array为null或undefined时,返回[]
if (array == null) {
return result
}

// 迭代
for (const value of array) {
// 深度大于0且可扁平化的value才执行下一步
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// 递归展平数组(受到调用堆栈数限制)。
baseFlatten(value, depth - 1, predicate, isStrict, result)
} else {
// 达到深度或完全展平后就push到result中
result.push(...value)
}
// 不可扁平化的值就按原样赋值给结果数组(isStrict为假的情况下)
} else if (!isStrict) {
// 不使用push是因为性能原因
// 参见https://segmentfault.com/q/1010000021808718
result[result.length] = value
}
}
// 把最后的result数组返回
return result
}

export default baseFlatten

引用的公开方法

isArguments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import getTag from './.internal/getTag.js'
import isObjectLike from './isObjectLike.js'

/**
* 检查value是否是一个类arguments对象
*
* @since 0.1.0
* @category Lang
* @param {*} value 要检查的值
* @returns {boolean} 如果 value 为一个类参数对象,那么返回 true,否则返回 false。
* @example
*
* isArguments(function() { return arguments }())
* // => true
*
* isArguments([1, 2, 3])
* // => false
*/
function isArguments(value) {
// 先检查是不是一个类对象,是的话再getTag,看看标签是否为[object Arguments]
return isObjectLike(value) && getTag(value) == '[object Arguments]'
}

export default isArguments

isObjectLike

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 检查 value 是否是 类对象。 如果一个值是类对象,那么它不应该是 null,
* 而且 typeof 后的结果是 "object"。
*
* @since 4.0.0
* @category Lang
* @param {*} value 要检查的值。
* @returns {boolean} 如果 value 为一个类对象,那么返回 true,否则返回 false。
* @example
*
* isObjectLike({})
* // => true
*
* isObjectLike([1, 2, 3])
* // => true
*
* isObjectLike(Function)
* // => false
*
* isObjectLike(null)
* // => false
*/
function isObjectLike(value) {
// 跟描述的完全一致,只要typeof返回object并且不为null,就返回true
return typeof value === 'object' && value !== null
}

export default isObjectLike

isLength

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** 用作各种`Number`类型常量的引用 */
const MAX_SAFE_INTEGER = 9007199254740991

/**
* 检查value是否为类数组的有效长度
*
* **No注意:** 这个方法大致基于
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @since 4.0.0
* @category Lang
* @param {*} value 要检查的值
* @returns {boolean} 如果是一个有效的长度则返回ture,否则返回false。
* @example
*
* isLength(3)
* // => true
*
* isLength(Number.MIN_VALUE)
* // => false
*
* isLength(Infinity)
* // => false
*
* isLength('3')
* // => false
*/
function isLength(value) {
// 首先必须是一个number
// 第二必须时大于等于0小于等于9007199254740991的整数
return typeof value === 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER
}

export default isLength

isArrayLike

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import isLength from './isLength.js'

/**
* 检查 value 是否是类数组。 如果一个值被认为是类数组,
* 那么它不是一个函数,并且value.length是个大于等于 0且小于等于 Number.MAX_SAFE_INTEGER的整数
*
* @since 4.0.0
* @category Lang
* @param {*} value 要检查的值。
* @returns {boolean} 如果value是一个类数组,那么返回 true,否则返回 false。
* @example
*
* isArrayLike([1, 2, 3])
* // => true
*
* isArrayLike(document.body.children)
* // => true
*
* isArrayLike('abc')
* // => true
*
* isArrayLike(Function)
* // => false
*/
function isArrayLike(value) {
// 不为null或undefined
// 不为function
// length属性满足isLength
return value != null && typeof value !== 'function' && isLength(value.length)
}

export default isArrayLike

isArrayLikeObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import isArrayLike from './isArrayLike.js'
import isObjectLike from './isObjectLike.js'

/**
* 此方法类似`isArrayLike`,但是它还同时检查value是否是个对象。
*
* @since 4.0.0
* @category Lang
* @param {*} value 要检查的值。
* @returns {boolean} 如果 value 是一个类数组对象,那么返回 true,否则返回 false。
* @example
*
* isArrayLikeObject([1, 2, 3])
* // => true
*
* isArrayLikeObject(document.body.children)
* // => true
*
* isArrayLikeObject('abc')
* // => false
*
* isArrayLikeObject(Function)
* // => false
*/
function isArrayLikeObject(value) {
// 检查是否是个类对象
// 再检查是否是个类数组
return isObjectLike(value) && isArrayLike(value)
}

export default isArrayLikeObject

last

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取array中的最后一个元素。
*
* @since 0.1.0
* @category Array
* @param {Array} array 要查询的数组
* @returns {*} 返回array的最后一项
* @example
*
* last([1, 2, 3])
* // => 3
*/
function last(array) {
// 先取到array的长度
const length = array == null ? 0 : array.length
// array[length - 1]就是最后一项,否则返回undefined
return length ? array[length - 1] : undefined
}

export default last

difference 系列

该系列的三个方法都用了三个核心内置方法:isArrayLikeObjectbaseDifferencebaseFlattenisArrayLikeObject主要用于忽略掉values中非类数组对象的value,baseDifference主要用执行排除的核心逻辑,baseFlatten用于把参数values合成为一个数组。

difference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import baseDifference from './.internal/baseDifference.js'
import baseFlatten from './.internal/baseFlatten.js'
import isArrayLikeObject from './isArrayLikeObject.js'

/**
* 创建一个具有唯一`array`值的数组,每个值不包含在其他给定的数组中。
* (注:即创建一个新数组,这个数组中的值,为第一个数字(array 参数)排除了给定数组中的值。)
* 该方法使用 [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)做相等比较。
* 结果值的顺序是由第一个数组中的顺序确定。
*
* **No注意:** 与 `pullAll`不同,这个方法会返回一个新数组。
*
* @since 0.1.0
* @category Array
* @param {Array} array 要检查的数组。
* @param {...Array} [values] 要排除的值的数组。
* @returns {Array} 返回一个过滤值后的新数组。
* @see union, unionBy, unionWith, without, xor, xorBy, xorWith,
* @example
*
* difference([2, 1], [2, 3])
* // => [1]
*/
function difference(array, ...values) {
// 首先判断array是否是一个类数组对象
return isArrayLikeObject(array)
// 是类数组对象,则使用baseDifference方法来排除
// baseFlatten中的depth=1,predicate为isArrayLikeObject,检查每一项是不是类数组对象,
// 本质上是吧values这个类数组扁平化成一个数组,方便与array进行比较
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))
// 非类数组对象则返回[]
: []
}

export default difference

differenceBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import baseDifference from './.internal/baseDifference.js'
import baseFlatten from './.internal/baseFlatten.js'
import isArrayLikeObject from './isArrayLikeObject.js'
import last from './last.js'

/**
* 此方法类似`difference` ,但会多接受一个 iteratee (注:迭代函数),
* 调用array 和 values 中的每个元素以产生比较的标准。
* 结果值是从第一数组中选择。iteratee 会调用一个参数:(value)。
* (首先使用迭代器分别迭代array 和 values中的每个元素,返回的值作为比较值)。
*
* **注意:** 与 `pullAllBy`不同, 此方法会返回一个新数组。
*
* @since 4.0.0
* @category Array
* @param {Array} array 要检查的数组
* @param {...Array} [values] 要排除的数组
* @param {Function} iteratee 每个元素调用的迭代函数
* @returns {Array} 返回一个过滤后的新数组
* @example
*
* differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor)
* // => [1.2]
*/
function differenceBy(array, ...values) {
// 从values中取最后一个元素为迭代函数
let iteratee = last(values)
// 如果最后一个元素是类数组对象,说明调用的时候没给迭代函数
// 那就直接给个undefined
if (isArrayLikeObject(iteratee)) {
iteratee = undefined
}
// 这里的调用方式就与difference方法一致了,只不过多了个iteratee参数
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), iteratee)
: []
}

export default differenceBy

differenceWith

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import baseDifference from './.internal/baseDifference.js'
import baseFlatten from './.internal/baseFlatten.js'
import isArrayLikeObject from './isArrayLikeObject.js'
import last from './last.js'

/**
* 此方法类似`difference` ,但是它接受一个 comparator (比较器),
* 调用比较器来比较array,values中的元素。
* 结果值是从第一个数组中选择。
* comparator 调用参数有两个:(arrVal, othVal)。
*
* **注意:** 与 `pullAllBy`不同, 此方法会返回一个新数组。
*
* @since 4.0.0
* @category Array
* @param {Array} array 要检查的数组
* @param {...Array} [values] 要排除的数组
* @param {Function} [comparator] 每个元素调用的比较器
* @returns {Array} 返回一个过滤后的新数组
* @example
*
* const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
*
* differenceWith(objects, [{ 'x': 1, 'y': 2 }], isEqual)
* // => [{ 'x': 2, 'y': 1 }]
*/
function differenceWith(array, ...values) {
// 从values中取最后一个元素为迭代函数
let comparator = last(values)
// 如果最后一个元素是类数组对象,说明调用的时候没给比较器
// 那就直接给个undefined
if (isArrayLikeObject(comparator)) {
comparator = undefined
}
// 这里的调用方式与difference方法一致,只不过iteratee参数为空,多了个comparator参数
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator)
: []
}

export default differenceWith

原生实现

last

虽然不知道为什么last这种也会专门有个方法,但是You-Dont-Need-Lodash-Underscore中也给出了原生的写法。

1
2
3
4
5
6
7
8
9
10
11

// 原生写法
const numbers = [1, 2, 3, 4, 5];
numbers[numbers.length - 1];
// => 5
// 或
numbers.slice(-1)[0]; // slice会返回一个新数组
// => 5
// 或
[].concat(numbers).pop()
// => 5

difference

简单的原生difference的实现是使用了reducefilter

  1. 首先array数组的第一个值([1, 2, 3, 4, 5])应该是对应于lodash-difference中的array参数,剩余的对应values
  2. reduce函数没有提供initialValue参数,所以第一个值为初始值。
  3. 每次循环时a为累计器,b为正在处理的元素。
  4. 每次循环中使用了filter对累计器a进行了筛选内循环。
  5. a的每一项c筛选时,都必须不能包含在b中。
  6. 完成。
1
2
3
4
5
// 原生写法
let arrays = [[1, 2, 3, 4, 5], [5, 2, 10]];
const result = arrays.reduce((a, b) => a.filter(c => !b.includes(c)));
console.log(result);
// output: [1, 3, 4]
👆 全文结束,棒槌时间到 👇