lodash源码解析:take家族

204 阅读6分钟

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

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

take

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。
Booleanif(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

/**
 * 创建一个数组,来源是裁剪数组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 其实都是裁剪字符串。

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

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

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 的区别罢了。

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

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 已经实现了对参数的各种判断。

// 没有做各种错误情况的判断,比如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);
}