0%

lodash源码解析:chunk、slice、toInteger、toFinite、toNumber、isObject、isSymbol

上一阵子一直在忙着对前端界做出一点微小的贡献,再过两个多月就能揭晓。现在打算开一个天坑,把 lodash 的源码挨个解析一遍,学习下 npm 下载量最大、依赖最多的库的源码逻辑。

解析的代码为 2020 年 7 月 18 日的lodash 源码,版本是4.17.15,fork 到了自己的仓库中,顺序按照官网文档的顺序,在解析时会将该方法依赖的子方法也会全部分析下。有很多自己难理解或理解错的地方,抛砖引玉。

chunk

文档地址:https://lodash.com/docs/4.17.15#chunk

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
import slice from './slice.js';
import toInteger from './toInteger.js';

/**
* 将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。
* 如果array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。
*
* @since 3.0.0
* @category Array
* @param {Array} array 需要处理的数组
* @param {number} [size=1] 每个数组区块的长度
* @returns {Array} 返回一个包含拆分区块的新数组(注:相当于一个二维数组)。
* @example
*
* chunk(['a', 'b', 'c', 'd'], 2)
* // => [['a', 'b'], ['c', 'd']]
*
* chunk(['a', 'b', 'c', 'd'], 3)
* // => [['a', 'b', 'c'], ['d']]
*/
function chunk(array, size = 1) {
// size必须大于等于0
size = Math.max(toInteger(size), 0);
// array为假时,length设为0;为真时设为数组长度
const length = array == null ? 0 : array.length;
// length为假或size为0时,返回空数组
if (!length || size < 1) {
return [];
}
// 初始化一个长度为(length / size并向上取整)的数组
let index = 0;
let resIndex = 0;
const result = new Array(Math.ceil(length / size));

// 循环取区块并赋值给result数组的对应位置
while (index < length) {
result[resIndex++] = slice(array, index, (index += size));
}
// 返回result
return result;
}

export default chunk;

slice

文档地址:https://lodash.com/docs/4.17.15#slice

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
47
48
49
50
51
52
53
54
55
56
57
/**
* 创建一个数组,来源是裁剪数组array,从 start 位置开始到 end 位置结束,但不包括 end 本身的位置。
*
* **注意:** 这个方法被用来代替
* [`Array#slice`](https://mdn.io/Array/slice)确保返回的是个稠密数组。
*
* @since 3.0.0
* @category Array
* @param {Array} array 要裁剪的数组
* @param {number} [start=0] 开始位置。负数索引将会被看作从数组结束位置的向前偏移。
* @param {number} [end=array.length] 结束位置。负数索引将会被看作从数组结束位置的向前偏移。
* @returns {Array} 返回剪切后的数组。
* @example
*
* var array = [1, 2, 3, 4]
*
* _.slice(array, 2)
* // => [3, 4]
*/
function slice(array, start, end) {
// array是否为undefined或null,是的话则length为0
let length = array == null ? 0 : array.length;
// length为假(undefined或0),则返回空数组
if (!length) {
return [];
}
// start是否为undefined或null,是的话则start赋值为0
start = start == null ? 0 : start;
// start是否为undefined,是的话则end赋值为length
end = end === undefined ? length : end;
// 如果start小于0
if (start < 0) {
// 防止真正的start变为负数
start = -start > length ? 0 : length + start;
}
// 防止end比length还大
end = end > length ? length : end;
// 如果end小于0
if (end < 0) {
end += length;
}
// 如果start大于end时,length赋值0,否则就使用>>>移位0确保length是个正整数
length = start > end ? 0 : (end - start) >>> 0;
// 确保start是个正整数
start >>>= 0;
// 返回结果初始化
let index = -1;
const result = new Array(length);
// 循环赋值
while (++index < length) {
result[index] = array[index + start];
}
// 返回
return result;
}

export default slice;

toInteger

文档地址:https://lodash.com/docs/4.17.15#toInteger

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
import toFinite from './toFinite.js';

/**
* 转换值为整数
*
* **注意:** 这个方法大致基于
* [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).
*
* @since 4.0.0
* @category Lang
* @param {*} value 需要转换的值
* @returns {number} 返回转换后的整数
* @see isInteger, isNumber, toNumber
* @example
*
* toInteger(3.2)
* // => 3
*
* toInteger(Number.MIN_VALUE)
* // => 0
*
* toInteger(Infinity)
* // => 1.7976931348623157e+308
*
* toInteger('3.2')
* // => 3
*/
function toInteger(value) {
// 转换为有限数字
const result = toFinite(value);
// 对1取余
const remainder = result % 1;
// 能取到余数就减去余数,返回了整数值
return remainder ? result - remainder : result;
}

export default toInteger;

toFinite

文档地址:https://lodash.com/docs/4.17.15#toFinite

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 toNumber from './toNumber.js';

/** 用作各种“数字”常量的引用。 */
const INFINITY = 1 / 0;
const MAX_INTEGER = 1.7976931348623157e308;

/**
* 将值转换为有限数字
*
* @since 4.12.0
* @category Lang
* @param {*} value 需要转换的值
* @returns {number} 返回转换后的数字
* @example
*
* toFinite(3.2)
* // => 3.2
*
* toFinite(Number.MIN_VALUE)
* // => 5e-324
*
* toFinite(Infinity)
* // => 1.7976931348623157e+308
*
* toFinite('3.2')
* // => 3.2
*/
function toFinite(value) {
// 先判断value是否假
if (!value) {
// 假的话就判断是否为0,为0返回0,否则返回value
return value === 0 ? value : 0;
}
// 把value转换为数字
value = toNumber(value);
// 判断是否正负无穷大
if (value === INFINITY || value === -INFINITY) {
// 是正负无穷大的话,则返回对应正负1.7976931348623157e+308
const sign = value < 0 ? -1 : 1;
return sign * MAX_INTEGER;
}
// value 如果!== value的情况,就是+0 === -0,此时返回0
return value === value ? value : 0;
}

export default toFinite;

toNumber

文档地址:https://lodash.com/docs/4.17.15#toNumber

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import isObject from './isObject.js';
import isSymbol from './isSymbol.js';

/** 用于各种Number类型的常量*/
const NAN = 0 / 0;

/** 用于匹配前面或者后面的空白 */
const reTrim = /^\s+|\s+$/g;

/** 用于检测错误的有符号十六进制字符串值 */
const reIsBadHex = /^[-+]0x[0-9a-f]+$/i;

/** 用于检测二进制字符串值 */
const reIsBinary = /^0b[01]+$/i;

/** 用于检测八进制字符串值 */
const reIsOctal = /^0o[0-7]+$/i;

/** 不依赖于root的内置方法引用 */
const freeParseInt = parseInt;

/**
* 将目标值转换为数字
*
* @since 4.0.0
* @category Lang
* @param {*} value 目标值
* @returns {number} 返回一个数字.
* @see isInteger, toInteger, isNumber
* @example
*
* toNumber(3.2)
* // => 3.2
*
* toNumber(Number.MIN_VALUE)
* // => 5e-324
*
* toNumber(Infinity)
* // => Infinity
*
* toNumber('3.2')
* // => 3.2
*/
function toNumber(value) {
// 如果为number原始类型,则直接返回原值,否则继续
if (typeof value === 'number') {
return value;
}
// 如果为symbol类型则返回NaN,否则继续
if (isSymbol(value)) {
return NAN;
}
// 如果为对象类型,则先判断valueof属性是否typeof结果为function
// 是的话就执行后赋值给other,否的话就valueof属性直接赋值给other
//
// 再判断other是否为对象,
// 是对象则把toString结果赋值给value,否的话就other赋值给value。
if (isObject(value)) {
const other = typeof value.valueOf === 'function' ? value.valueOf() : value;
value = isObject(other) ? `${other}` : other;
}
// 如果当前的value是否为string类型,
// 不是string类型的话就判断是否为0,
// 为0则直接返回,不为0就强制转换后返回
if (typeof value !== 'string') {
return value === 0 ? value : +value;
}
// 现在已经确定为string类型,掐头去尾
value = value.replace(reTrim, '');
// 判断是否为二进制
const isBinary = reIsBinary.test(value);
// 如果是二进制或八进制,就直接转换为数字返回
return isBinary || reIsOctal.test(value)
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: // 不是的话就判断是否为有错的16进制,是的话返回Nan,否就转化为数字返回
reIsBadHex.test(value)
? NAN
: +value;
}

export default toNumber;

isObject

文档地址:https://lodash.com/docs/4.17.15#isObject

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
/**
* 检查value是否为
* [Object类型](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* 例如 arrays, functions, objects, regexes, `new Number(0)`, and `new String('')
* @since 0.1.0
* @category Lang
* @param {*} value 需要检查的值
* @returns {boolean} 如果对象则返回true,不是返回false
* @example
*
* isObject({})
* // => true
*
* isObject([1, 2, 3])
* // => true
*
* isObject(Function)
* // => true
*
* isObject(null)
* // => false
*/
function isObject(value) {
// 本质上使用typeof判断符来判断,这是以下类型的返回值,所以需要先排除null,并把function纳入进来
// typeof null 'object'
// typeof function() {} 'function'
// typeof {} 'object'
// typeof [] 'object'
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
}

export default isObject;

isSymbol

文档地址:https://lodash.com/docs/4.17.15#isSymbol

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
import getTag from './.internal/getTag.js';

/**
* 检查目标值是否为Symbol原始类型或Symbol对象
*
* @since 4.0.0
* @category Lang
* @param {*} value 需要检查的值
* @returns {boolean} 如果为symbol则返回true,否则返回false
* @example
*
* isSymbol(Symbol.iterator)
* // => true
*
* isSymbol('abc')
* // => false
*/
function isSymbol(value) {
// || 符号之后的判断是为了在ES2015之前的代码polyfill中检测symbol
const type = typeof value;
return (
type == 'symbol' ||
(type === 'object' && value != null && getTag(value) == '[object Symbol]')
);
}

export default isSymbol;

getTag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const toString = Object.prototype.toString;

/**
* 获取目标值的类型标签(Symbol.toStringTag)
*
* @private
* @param {*} value 需要查询的值
* @returns {string} 返回类型标签.
*/
function getTag(value) {
// 如果==为假,则先检查是否为undefined,
// 是的话返回[object Undefined],不是的话统统返回[object Null]
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]';
}
// 如==为真,直接借用Object原型链上的toString方法
return toString.call(value);
}

export default getTag;

纯JS实现

在找lodash源码的时候发现了一个很有意思的仓库叫You-Dont-Need-Lodash-Underscore,使用纯JS实现了Lodash/Underscore的很多方法。在能明确自己的变量类型并且不想很重的引入lodash时可以自己写这些方法,同样来解析下它。

chunk

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
// Underscore/Lodash使用
_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]

_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]


// 原生实现

const chunk = (input, size) => {
// arr是累计器
// item是当前当前值
// idx是当前索引
// []是初始累计器
// 整体的逻辑如下:
// 当索引对size取余为0时,就在当前累计器的基础上加一个子数组[item]
// 当索引对size取余不为0时,就在当前累计器基础上把最后一个子数组加上子元素item
return input.reduce((arr, item, idx) => {
return idx % size === 0
? [...arr, [item]]
: [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];// 这里活用了slice不改变原数组,返回新数组的规则
}, []);
};

chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]

chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

slice

slice就很尴尬了,Array.prototype上的原生方法。

1
2
3
4
5
6
7
8
9
// Lodash
var array = [1, 2, 3, 4]
console.log(_.slice(array, 1, 3))
// output: [2, 3]

// 原生
var array = [1, 2, 3, 4]
console.log(array.slice(1, 3));
// output: [2, 3]

👆 全文结束,棒槌时间到 👇