0%

lodash源码解析:take家族

本篇分析下 take 家族的方法,该系列的方法主要是用来从数组的两端开始提取切片。包括taketakeWhiletakeRighttakeRightWhile以及核心方法baseWhileslice,并根据 ECMAScript 标准分析下>>> 0逻辑右移的作用。

具体的依赖路径图如下所示:

Number 类型

JS 中存储时是不区分小数和整数的。所有的 Number 类型,都是用 IEEE-754标准中的双精度浮点数存储。组成方式如下:

双精度浮点数

但是某些运算只有整数才能完成,此时 JavaScript 会自动把 64 位浮点数,转成 32 位有符号整数,然后再进行运算,比如位运算。

位运算符只对整数起作用,如果不是整数,会自动转为 32 位有符号整数后再执行。

逻辑右移

之前的文章中分析过 slicebaseWhile,在这里再拿出来的目的主要是分析下其中用到的逻辑右移。

ECMA262 中关于逻辑右移的内容如下:

The Unsigned Right Shift Operator ( >>> )

NOTE: Performs a zero-filling bitwise right shift operation on the left operand by the amount specified by the right operand.

Runtime Semantics:

Evaluation : ShiftExpression >>> AdditiveExpression

  1. Let lref be the result of evaluating ShiftExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating AdditiveExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lnum be ? ToNumeric(lval).
  6. Let rnum be ? ToNumeric(rval).
  7. If Type(lnum) is different from Type(rnum), throw a TypeError exception.
  8. Let T be Type(lnum).
  9. Return T::unsignedRightShift(lnum, rnum).

Number::unsignedRightShift ( x, y )

  1. Let lnum be ! ToInt32(x).
  2. Let rnum be ! ToUint32(y).
  3. Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
  4. Return the result of performing a zero-filling right shift of lnum by shiftCount bits. Vacated bits are filled with zero. The result is an unsigned 32-bit integer.

翻译下:

无符号右移操作符( >>> )

注意:在左操作数上,按右操作数所指定数量的 0 填充位,来完成右移操作。

运行时语义:

计算 : ShiftExpression >>> AdditiveExpression

  1. lrefShiftExpression 计算的结果。
  2. lval 为 ? GetValue(lref)。
  3. rrefAdditiveExpression 计算的结果。
  4. rval 为 ? GetValue(rref)。
  5. lnum 为 ? ToNumeric(lval)。
  6. rnum 为 ? ToNumeric(rval)。
  7. 如果 Type(lnum) 与 Type(rnum) 不同, 则抛出 TypeError 错误。
  8. 让 T 为 Type(lnum)。
  9. 返回 T::unsignedRightShift(lnum, rnum)

Number::unsignedRightShift ( x, y )

  1. lnum 为 ! ToInt32(x)。
  2. rnum 为 ! ToUint32(y)。
  3. 遮蔽除 rnum 最低有效位 5 位以外的所有内容,结果设为 shiftCount ,即掩码运算 rnum & 0x1F 的计算结果。(ps.这里是防止右操作数过大超过 32 位)
  4. 返回由 shiftCount 执行 lnum 数量的零填充右移的结果。空位用零填充。结果是一个无符号 32整数

x >>> 0

在核心方法 slice 中,用到了 (end - start) >>> 0,所以可以根据标准看看到底有什么用。

先看看前文第 5 条,用 ToNumeric 方法将 lnum 转化为数字。下面的表格是ECMA262标准ToNumeric如何将各种类型转化为数字。

参数类型 结果
Undefined 返回 NaN。
Null 返回 +0。
Boolean if(true) return 1; if(false) return +0;
Number 返回参数 (不转化)。
String 能转化为数字就返回对应数字,否则就返回 NaN
Symbol 抛出 TypeError 错误。
BigInt 抛出 TypeError 错误。
Object 应用如下步骤: 1. 让 primValue 为 ? ToPrimitive(argument, hint Number)。 2. 返回 ? ToNumber(primValue)。

可以发现转化为数字后,左操作数就变为了 NaN 或者数字,同时第 8 条又排除了 NaN,所以第 8 条执行完后左操作数是绝对的数字类型 NumberBigInt。

接下来执行第 9unsignedRightShift ( x, y )方法,在这个方法中先把左操作数变为 32 位有符号整数,右操作数变为 32 位无符号整数,再执行 0 填充右移。

所以如果右操作数为 0 时,就直接把左操作数最高位设为 0,其余不动。

通过以上操作后,不管左操作数是什么类型,是正是负,是小数还是整数,统统变成了非负整数。

举个例子,-1.1(64位浮点) -> -1(32位有符号整形) = 11111......111(位) -> 01111......111(逻辑右移0位) -> 4294967295(32位有符号整形) -> 4294967295(64位浮点)

核心方法

slice 方法在之前的文章中分析过,现在再拿出来主要是看看优秀 JS 库对参数的严格判断。在 slice 真正的裁剪功能实现前,用了大量的篇幅去进行参数的判断和转化,包括如下的判断:

  1. 数组的存在和数组长度的判断。
  2. start 的存在、start 是否小于 0startlength 的大小,计算转化为标准 start
  3. end 的存在 end 是否小于 0endlength 的大小,计算转化为标准 length
  4. startlength 必须为非负整数。

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;

baseWhile

baseWhile 是对 slice 方法的封装,不管是 drop 还是 take 其实都是裁剪字符串。

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

/**
* 比如`dropWhile` 和 `takeWhile`之类的方法的基础实现
*
* @private
* @param {Array} array 要查询的数组
* @param {Function} predicate 每次迭代时调用的函数
* @param {boolean} [isDrop] 指示丢弃还是保留元素。
* @param {boolean} [fromRight] 指示从右向左迭代
* @returns {Array} 返回剪切后的数组
*/
function baseWhile(array, predicate, isDrop, fromRight) {
// 获取数组长度
const { length } = array;
// 根据fromRight获取起始位置
let index = fromRight ? length : -1;

// 开始迭代,把运算都写到了迭代条件里了
// 从头到尾或从尾到头是给index规定了个[0,length-1]的范围
// && 符号后面的内容是看看什么时候predicate返回假值就结束,就可以拿到当前的index了
while (
(fromRight ? index-- : ++index < length) &&
predicate(array[index], index, array)
) {}

// 真值表
// idDrop为真,fromRight为真,就把[index,length]内容删掉;
// idDrop为真,fromRight为假,就把[0,index]内容删掉;
// idDrop为假,fromRight为真,就把[index,length]内容保留;
// idDrop为假,fromRight为假,就把[0,index]内容保留;
return isDrop
? slice(array, fromRight ? 0 : index, fromRight ? index + 1 : length)
: slice(array, fromRight ? index + 1 : 0, fromRight ? length : index);
}

export default baseWhile;

take 家族

taketakeRight 这两个方法可以放在一起说,都是简单封装的 slice,提前计算出起始和结束的 index 即可。

take

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

/**
* 创建一个`array`的切片,从`array`的起始位置开始提取`n`个元素。
*
* @since 0.1.0
* @category Array
* @param {Array} array 要查询的数组。
* @param {number} [n=1] 要提取的元素个数。
* @returns {Array} 返回`array`的切片。
* @example
*
* take([1, 2, 3])
* // => [1]
*
* take([1, 2, 3], 2)
* // => [1, 2]
*
* take([1, 2, 3], 5)
* // => [1, 2, 3]
*
* take([1, 2, 3], 0)
* // => []
*/
function take(array, n = 1) {
// 默认提取一个
if (!(array != null && array.length)) {
return [];
}
// 本质上使用的是slice提取的
// n小于0时,就设为0
return slice(array, 0, n < 0 ? 0 : n);
}

export default take;

takeRight

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

/**
* 创建一个`array`的切片,从`array`的结束位置向左提取`n`个元素。
*
* @since 3.0.0
* @category Array
* @param {Array} array 要查询的数组。
* @param {number} [n=1] 要提取的元素个数。
* @returns {Array} 返回 array 数组的切片。
* @example
*
* takeRight([1, 2, 3])
* // => [3]
*
* takeRight([1, 2, 3], 2)
* // => [2, 3]
*
* takeRight([1, 2, 3], 5)
* // => [1, 2, 3]
*
* takeRight([1, 2, 3], 0)
* // => []
*/
function takeRight(array, n = 1) {
const length = array == null ? 0 : array.length;
if (!length) {
return [];
}
// 这里是核心,用length-n将切片的开始位置转化为index
n = length - n;
// 从n提取到末尾
return slice(array, n < 0 ? 0 : n, length);
}

export default takeRight;

takeWhile

takeWhiletakeRightWhile 也是对 baseWhile 的简单封装,第三个参数 isDrop 都是传的 false,只是第四个参数 fromRight 的区别罢了。

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

/**
* 从 `array` 的起始位置开始向右提取元素,直到 `predicate` 断言返回假值。
* predicate 会传入三个参数: (value, index, array)。
* @since 3.0.0
* @category Array
* @param {Array} array The array to query.要查询的数组
* @param {Function} predicate The function invoked per iteration.每次迭代调用的函数。
* @returns {Array} Returns the slice of `array`.返回 array 数组的切片。
* @example
*
* const users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': true },
* { 'user': 'pebbles', 'active': false }
* ]
*
* takeWhile(users, ({ active }) => active)
* // => objects for ['barney', 'fred']
*/
function takeWhile(array, predicate) {
return array != null && array.length
? // baseWhile(array, predicate, isDrop, fromRight)
baseWhile(array, predicate)
: [];
}

export default takeWhile;

takeRightWhile

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

/**
* 从 `array` 的结束位置开始向左提取元素,直到 `predicate` 断言返回假值。
* predicate 会传入三个参数: (value, index, array)。
*
* @since 3.0.0
* @category Array
* @param {Array} array 要查询的数组。
* @param {Function} predicate 每次迭代调用的断言函数。
* @returns {Array} 返回 `array` 的切片。
* @example
*
* const users = [
* { 'user': 'barney', 'active': false },
* { 'user': 'fred', 'active': true },
* { 'user': 'pebbles', 'active': true }
* ]
*
* takeRightWhile(users, ({ active }) => active)
* // => objects for ['fred', 'pebbles']
*/
function takeRightWhile(array, predicate) {
return array != null && array.length
? // baseWhile(array, predicate, isDrop, fromRight)
baseWhile(array, predicate, false, true)
: [];
}

export default takeRightWhile;

原生实现

taketakeRight 本来就是用封装的 lodashslice 方法,所以原生实现时直接调用 slice 即可,原生的 slice 已经实现了对参数的各种判断。

1
2
3
4
5
6
7
8
// 没有做各种错误情况的判断,比如n < 0
function take(array = [], n = 0) {
return array.slice(0, n);
}

function takeRight(array = [], n = 0) {
return array.slice(n <= array.length ? array.length - n : 0);
}
👆 全文结束,棒槌时间到 👇