0%

lodash源码解析:reject、remove、repeat、replace、result、round

本篇按顺序看R开头的几个零散的小方法,包括rejectremoverepeatreplaceresultround。先分析 lodash 中对变参函数的处理,最后给出几个方法的原生实现。

对变参的处理

这几个方法中random实现比较有意思,分以下几种情况:

  1. 当不传参数时,返回[ 0, 1 ]之间的整数;
  2. 当传1个参数时,
    • 数字,返回[ 0, arguments[0] ]
    • 布尔值,根据真假返回[ 0, 1 ]之间的整数或浮点数;
  3. 2个参数时,
    • 两个数字,返回[arguments[0], arguments[1] ]之间的随机整数;
    • 一个数字和一个布尔值,根据真假返回[ 0, arguments[0] ]之间的整数或浮点数;
  4. 当传3个参数时,前两个数字,后一个布尔值,根据arguments[2]决定返回[arguments[0], arguments[1] ]之间的整数或者浮点数。

lodash 实现时的思路如下:

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
function random(lower, upper, floating) {
// 判断给的参数够不够三个,在不够的情况下
if (floating === undefined) {
// 下面说明给了两个参数并且第二个是布尔值,修改参数为lower(不变), undefined, floating(原upper)
if (typeof upper === 'boolean') {
floating = upper;
upper = undefined;
}
// 下面说明给了一个参数并且是布尔值,修改参数为undefined, undefined, floating(原lower)
else if (typeof lower === 'boolean') {
floating = lower;
lower = undefined;
}
}

// 当参数为undefined, undefined, floating时,也就是只给了一个floating参数或者没给参数时
if (lower === undefined && upper === undefined) {
// 将参数修改为0, 1, floating,返回0,1之间的值
lower = 0;
upper = 1;
} else {
// 下面的情况是至少有两个参数
// 将lower改为有限数字
lower = toFinite(lower);
if (upper === undefined) {
// 有两个参数的情况,把upper改为lower,lower改为0
upper = lower;
lower = 0;
} else {
// 有三个参数时,就将参数有限化
upper = toFinite(upper);
}
}
// 经过上面一系列处理,如果lower比upper大,就交换下
if (lower > upper) {
const temp = lower;
lower = upper;
upper = temp;
}

// 处理到这里时,已经变成正规的全参数的输入了。下面就是对random逻辑的处理

// 如果floating为真,或者lower upper为浮点数,就返回浮点数
if (floating || lower % 1 || upper % 1) {
// 0-1的随机数
const rand = Math.random();
// 随机数长度
const randLength = `${rand}`.length - 1;
// 返回随机浮点数
// freeParseFloat(`1e-${randLength}`)的操作是为了确保必为浮点数。
// ??存疑当rand为0时还是返回0
return Math.min(
lower + rand * (upper - lower + freeParseFloat(`1e-${randLength}`)),
upper
);
}
// 否则返回整数随机数,常规随机数操作
return lower + Math.floor(Math.random() * (upper - lower + 1));
}

上述的 lodash 参数处理百转千回,如果是我处理可能按照之前分类的情况写,用参数长度(length)和类型(typeof)来区分不同的情况:

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
function random(...args) {
let lower, upper, floating;
const length = args.length;
// 没有参数
if (length === 0) {
lower = 0;
upper = 1;
floating = false;
// 一个参数
} else if (length === 1) {
lower = 0;
if (typeof args[0] === 'boolean') {
upper = 1;
floating = args[0];
} else {
upper = args[0];
floating = false;
}
// 两个参数
} else if (length === 2) {
if (typeof args[1] === 'boolean') {
lower = 0;
upper = args[0];
floating = args[1];
} else {
lower = args[0];
upper = args[1];
floating = false;
}
// 三个参数
} else if (length >= 3) {
lower = args[0];
upper = args[1];
floating = args[2];
}
// 比较大小并交换
lower = toFinite(lower);
upper = toFinite(upper);
if (lower > upper) {
const temp = lower;
lower = upper;
upper = temp;
}

// 下面的业务处理就相同了

// 如果floating为真,或者lower upper为浮点数,就返回浮点数
if (floating || lower % 1 || upper % 1) {
// 0-1的随机数
const rand = Math.random();
// 随机数长度
const randLength = `${rand}`.length - 1;
// 返回随机浮点数
// freeParseFloat(`1e-${randLength}`)的操作是为了确保必为浮点数。
// ??存疑当rand为0时还是返回0
return Math.min(
lower + rand * (upper - lower + freeParseFloat(`1e-${randLength}`)),
upper
);
}
// 否则返回整数随机数,常规随机数操作
return lower + Math.floor(Math.random() * (upper - lower + 1));
}

试验了下,和lodashrandom执行结果一致

依赖的 lodash 方法

negate

negate是一个参数类型为function,返回结果类型也是function的函数,最终目的就是对参数返回的结果取反。

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
/**
* 创建一个针对断言函数 `func` 结果取反的函数。
* `func` 断言函数被调用的时候,this 绑定到创建的函数,并传入对应参数。
*
* @since 3.0.0
* @category Function
* @param {Function} predicate 需要对结果取反的断言函数。
* @returns {Function} 返回一个新的取反函数。
* @example
*
* function isEven(n) {
* return n % 2 == 0
* }
*
* filter([1, 2, 3, 4, 5, 6], negate(isEven))
* // => [1, 3, 5]
*/
function negate(predicate) {
// predicate如果不是函数就报错
if (typeof predicate !== 'function') {
throw new TypeError('Expected a function');
}
// 返回一个匿名函数
return function (...args) {
// 返回的匿名这个函数执行后会返回(predicate函数的执行结果)取反,
// 执行predicate时把this绑定到该匿名函数上,顺带带上参数
return !predicate.apply(this, args);
};
}

export default negate;

filter

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
/**
* 遍历 array(数组)元素,返回 predicate(断言函数)返回真值 的所有元素的数组。
* predicate(断言函数)调用三个参数:(value, index, array)。
*
* **注意:** 与`remove`不同,此方法会返回一个新数组
*
* @since 5.0.0
* @category Array
* @param {Array} array 要迭代的数组
* @param {Function} predicate 每次迭代调用的断言函数
* @returns {Array} 返回新的筛选过的数组
* @see pull, pullAll, pullAllBy, pullAllWith, pullAt, remove, reject
* @example
*
* const users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false }
* ]
*
* filter(users, ({ active }) => active)
* // => objects for ['barney']
*/
function filter(array, predicate) {
// 各种初始化操作
let index = -1;
let resIndex = 0;
const length = array == null ? 0 : array.length;
const result = [];

// 迭代
while (++index < length) {
// value为每一个元素
const value = array[index];
// 执行断言函数,返回真时,就push到result种
if (predicate(value, index, array)) {
result[resIndex++] = value;
}
}
// 返回结果
return result;
}

export default filter;

filterObject

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
/**
* 迭代`object`的属性,返回 predicate(断言函数)返回真值 的所有元素的数组。
* predicate(断言函数)调用三个参数:(value, index, array)。
*
* 如果想返回一个对象,请看`pickBy`
*
* @since 5.0.0
* @category Object
* @param {Object} object 要迭代的对象
* @param {Function} predicate 每次迭代调用的函数
* @returns {Array} 返回一个新的过滤后的数组
* @see pickBy, pull, pullAll, pullAllBy, pullAllWith, pullAt, remove, reject
* @example
*
* const object = { 'a': 5, 'b': 8, 'c': 10 }
*
* filterObject(object, (n) => !(n % 5))
* // => [5, 10]
*/
function filterObject(object, predicate) {
// 初始化操作
object = Object(object);
const result = [];

// 迭代每一个键
Object.keys(object).forEach((key) => {
// value为每一个键对应的值
const value = object[key];
// 当断言函数返回真时,push到result种
if (predicate(value, key, object)) {
result.push(value);
}
});
return result;
}

export default filterObject;

lodash 方法

random

random方法实际上是对Math.random的封装,可以返回固定范围内的整数或小数。

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
82
83
84
85
86
87
88
89
import toFinite from './toFinite.js';

/** 内置方法引用,不依赖于`root` */
const freeParseFloat = parseFloat;

/**
* 产生一个包括 lower 与 upper 之间的数。
* 如果只提供一个参数返回一个0到提供数之间的数。
* 如果 floating 设为 true,或者 lower 或 upper 是浮点数,结果返回浮点数。
*
* **注意:** JavaScript 遵循 IEEE-754 标准处理无法预料的浮点数结果。
*
* @since 0.7.0
* @category Number
* @param {number} [lower=0] 下限
* @param {number} [upper=1] 上限
* @param {boolean} [floating] 指定是否返回浮点数
* @returns {number} 返回随机数
* @see uniqueId
* @example
*
* random(0, 5)
* // => 0到5之间的整数
*
* random(5)
* // => 也是0到5之间的整数
*
* random(5, true)
* // => 0到5之间的浮点数
*
* random(1.2, 5.2)
* // => 1.2到5.2之间的浮点数
*/
function random(lower, upper, floating) {
// 判断给的参数够不够,在不够的情况下
if (floating === undefined) {
// 说明给了两个参数,修改参数为lower, undefined, floating
if (typeof upper === 'boolean') {
floating = upper;
upper = undefined;
}
// 说明给了一个参数,修改参数为undefined, undefined, floating
else if (typeof lower === 'boolean') {
floating = lower;
lower = undefined;
}
}
if (lower === undefined && upper === undefined) {
// 当参数为undefined, undefined, floating时,将参数修改为0, 1, floating
lower = 0;
upper = 1;
} else {
// 下面的情况是至少有两个参数
// 将lower改为有限数字
lower = toFinite(lower);
if (upper === undefined) {
// 有两个参数的情况,把upper改为lower,lower改为0
upper = lower;
lower = 0;
} else {
// 有三个参数时,就将参数有限化
upper = toFinite(upper);
}
}
// 经过上面一系列处理,如果lower比upper大,就交换下
if (lower > upper) {
const temp = lower;
lower = upper;
upper = temp;
}
// 如果floating为真,或者lower upper为浮点数,就返回浮点数
if (floating || lower % 1 || upper % 1) {
// 0-1的随机数
const rand = Math.random();
// 随机数长度
const randLength = `${rand}`.length - 1;
// 返回随机浮点数
// freeParseFloat(`1e-${randLength}`)的操作是为了确保必为浮点数。
// ??存疑当rand为0时还是返回0
return Math.min(
lower + rand * (upper - lower + freeParseFloat(`1e-${randLength}`)),
upper
);
}
// 否则返回整数随机数,常规随机数操作
return lower + Math.floor(Math.random() * (upper - lower + 1));
}

export default random;

reject

reject是利用了negate实现的filter的相反方法。

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 filter from './filter.js';
import filterObject from './filterObject.js';
import negate from './negate.js';

/**
* `filter`的相反方法,
* 此方法 返回 predicate(断言函数) 不 返回 truthy(真值)的collection(集合)元素。
*
* @since 0.1.0
* @category Collection
* @param {Array|Object} collection 要迭代的集合
* @param {Function} predicate 每次迭代时调用的函数
* @returns {Array} 返回新的过滤后的数组
* @see pull, pullAll, pullAllBy, pullAllWith, pullAt, remove, filter
* @example
*
* const users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false }
* ]
*
* reject(users, ({ active }) => active)
* // => objects for ['fred']
*/
function reject(collection, predicate) {
// 当collection参数是数组时使用filter方法,是集合时使用filterObject方法
const func = Array.isArray(collection) ? filter : filterObject;
// 过滤参数其实是对所有的predicate取反,实现过滤掉所有为真的元素
return func(collection, negate(predicate));
}

export default reject;

remove

remove方法在原数组中删除元素,并返回被删掉的元素组成的数组。

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

/**
* 移除数组中predicate(断言)返回为真值的所有元素,并返回移除元素组成的数组。
* predicate(断言) 会传入3个参数: (value, index, array)。
*
* **注意:** 和 `filter`不同, 这个方法会改变数组 `array`。使用`pull`来根据提供的value值从数组中移除元素。
*
* @since 2.0.0
* @category Array
* @param {Array} array 要修改的数组
* @param {Function} predicate 每次迭代调用的函数
* @returns {Array} 返回移除元素组成的新数组
* @see pull, pullAll, pullAllBy, pullAllWith, pullAt, reject, filter
* @example
*
* const array = [1, 2, 3, 4]
* const evens = remove(array, n => n % 2 == 0)
*
* console.log(array)
* // => [1, 3]
*
* console.log(evens)
* // => [2, 4]
*/
function remove(array, predicate) {
// 初始化
const result = [];
// 条件不满足就直接返回空数组
if (!(array != null && array.length)) {
return result;
}
// 继续初始化
let index = -1;
const indexes = [];
const { length } = array;

// 迭代
while (++index < length) {
// 取到数组每个元素值
const value = array[index];
// 如果断言返回真
if (predicate(value, index, array)) {
// 就把 value push到result,组成了返回的结果
result.push(value);
// 把index push到indexes,便于使用pullAt
indexes.push(index);
}
}
// 在原数组中删掉了对应元素
basePullAt(array, indexes);
// 把被删掉元素组成的数组返回
return result;
}

export default remove;

repeat

repeat方法利用了平方求幂算法进行了快速重复。

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
/**
* 重复 `n` 次给定字符串。
*
* @since 3.0.0
* @category String
* @param {string} [string=''] 要重复的字符串。
* @param {number} [n=1] 重复的次数。
* @returns {string} 返回重复的字符串。
* @example
*
* repeat('*', 3)
* // => '***'
*
* repeat('abc', 2)
* // => 'abcabc'
*
* repeat('abc', 0)
* // => ''
*/
function repeat(string, n) {
let result = '';
// 当参数为空,或者重复次数不合理,直接返回空字符串
if (!string || n < 1 || n > Number.MAX_SAFE_INTEGER) {
return result;
}
// 利用平方求幂算法,以实现更快的重复。
// 具体细节见 https://en.wikipedia.org/wiki/Exponentiation_by_squaring
do {
// 奇数
if (n % 2) {
// result就加一个当前字符串
result += string;
}
n = Math.floor(n / 2);
if (n) {
// 当 n > 0 时,就字符串翻倍
string += string;
}
} while (n);

return result;
}

export default repeat;

replace

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
/**
* 将`string`字符串中匹配的`pattern`替换为给定的`replacement`
*
* **注意:** 该方法基于 [`String#replace`](https://mdn.io/String/replace).
*
* @since 4.0.0
* @category String
* @param {string} [string=''] 要修改的字符串
* @param {RegExp|string} pattern 要匹配的内容
* @param {Function|string} replacement 替换的内容
* @returns {string} 返回替换后的字符串
* @see truncate, trim
* @example
*
* replace('Hi Fred', 'Fred', 'Barney')
* // => 'Hi Barney'
*/
function replace(...args) {
// string是第一个参数,而且要进行字符串化
const string = `${args[0]}`;
// 参数少于三个时,直接返回string
// 否则,就调用String.prototype.replace方法
return args.length < 3 ? string : string.replace(args[1], args[2]);
}

export default replace;

result

result是一个类似get的方法,实现的方式很有意思,尽量少的定义变量。

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

/**
* 这个方法类似`get`, 但是如果解析到的值是一个函数的话,
* 就绑定 `this` 到这个函数并返回执行后的结果。
*
* @since 0.1.0
* @category Object
* @param {Object} object 要查询的对象
* @param {Array|string} path 要解析的属性路径
* @param {*} [defaultValue] 解析到`undefined`后返回的默认值
* @returns {*} 返回解析后的值
* @example
*
* const object = { 'a': [{ 'b': { 'c1': 3, 'c2': () => 4 } }] }
*
* result(object, 'a[0].b.c1')
* // => 3
*
* result(object, 'a[0].b.c2')
* // => 4
*
* result(object, 'a[0].b.c3', 'default')
* // => 'default'
*
* result(object, 'a[0].b.c3', () => 'default')
* // => 'default'
*/
function result(object, path, defaultValue) {
// 把path转为路径数组
path = castPath(path, object);

let index = -1;
let length = path.length;

// 当path为空的时候确保能进入循环
if (!length) {
length = 1;
object = undefined;
}
// 迭代
while (++index < length) {
// object不为空时,value设为取object的每一级
// toKey(path[index])是获取当前级的键
let value = object == null ? undefined : object[toKey(path[index])];
// 只要有某一级value为undefined时,打破循环,直接等于defaultValue
if (value === undefined) {
index = length;
value = defaultValue;
}
// 当某一级value是函数时,直接执行value.call(object)并赋值给object,
// 否则把value直接赋值给object,object因此被降级
object = typeof value === 'function' ? value.call(object) : value;
}
// 把最后的object返回
return object;
}

export default result;

round

round方法是实现带精度的四舍五入,本质上利用的是createRound这个方法实现的。

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

/**
* 根据 `precision`(精度) 四舍五入 `number`。
*
* @since 3.10.0
* @category Math
* @param {number} number 要四舍五入的数字。
* @param {number} [precision=0] .四舍五入的精度。
* @returns {number} 返回四舍五入后的数字。
* @example
*
* round(4.006)
* // => 4
*
* round(4.006, 2)
* // => 4.01
*
* round(4060, -2)
* // => 4100
*/
// 调用createRound返回的方法
const round = createRound('round');

export default round;

createRound()方法,参数可以为floorceilround,返回不同的近似函数

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
/**
* 创建一个函数,比如`round`
*
* @private
* @param {string} methodName 当求近似时使用的`Math`方法名
* @returns {Function} 返回一个新的求近似的函数
*/
function createRound(methodName) {
// 选择一种近似方法,floor,ceil,round
const func = Math[methodName];
// 返回一个函数
return (number, precision) => {
// 当精度precision不存在时,赋值为0
// precision大于等于0时,不能超过292
// precision小于0时,不能小于-292
precision =
precision == null
? 0
: precision >= 0
? Math.min(precision, 292)
: Math.max(precision, -292);
// 当精度不为0时
if (precision) {
// 用指数表示法转换以避免浮点问题。
// 查看 [MDN](https://mdn.io/round#Examples) 更多细节.

// 把数字split,pair[0]为系数,pair[1]为指数 比如(9876543211234567899876, -20)
// 9876543211234567899876也就是98765432112345678e+21
let pair = `${number}e`.split('e'); // pair => ['9.876543211234568', '+21']
const value = func(`${pair[0]}e${+pair[1] + precision}`); // value => func(9.8765432112345678e1) => 99
pair = `${value}e`.split('e'); // pair => ['99', '']
// 在这一步恢复了原来的幂指数
return +`${pair[0]}e${+pair[1] - precision}`; // 99e20
}
// 精度为0就直接调用Math的原生方法
return func(number);
};
}

export default createRound;

上面的情况说明的是带 e 显示的浮点数,下面再看看不带 e 的处理过程。

1
2
3
4
5
6
// (9.876, 2)
let pair = `${number}e`.split('e'); // pair => ['9.876', '']
const value = func(`${pair[0]}e${+pair[1] + precision}`); // value => func(9.876e2) => 988
pair = `${value}e`.split('e'); // pair => ['988', '']
// 在这一步恢复了原来的幂指数
return +`${pair[0]}e${+pair[1] - precision}`; // 988e-2 =>9.88

原生实现

repeatreplaceroundECMAScriptString.prototype 原生已经实现了的,直接使用即可。removeresult原生实现时也得和lodash差不多的思路。下面直接分析下 reject 的原生实现。

reject

reject 方法是一个类似 filter 的方法,只不过结果与 filter 完全相反。所以可以通过创建一个 complement 函数,该函数接收一个函数 f 作为参数,返回一个新的函数新的函数执行时返回的结果其实是!f(...args),实现了功能。

1
2
3
4
const reject = function (arr, predicate) {
const complement = (f) => (...args) => !f(...args);
return arr.filter(complement(predicate));
};
👆 全文结束,棒槌时间到 👇