lodash源码解析:pull家族

1,535 阅读8分钟

本篇分析下pull家族的方法,包括pullpullAllpullAllBypullAllWithpullAt以及核心方法basePullAllbasePullAt。顺便分析依赖到的 lodash 方法memoize,最后给出pull方法的原生实现。

另外,在 basePullAt 方法的源码中发现一个 bug,在数组中多次列出同一个要删除的元素索引的情况下,会导致方法执行结果不对,删除元素过多。但是在打包好文件中将 let 变为了 var 并挪动了位置,掩盖了这个 bug

依赖的内置方法

baseAt

import get from '../get.js';

/**
 * `at`方法的基础实现,不用支持单个的路径
 *
 * @private
 * @param {Object} object 要迭代的对象
 * @param {string[]} paths 要获取的对象的元素路径数组
 * @returns {Array} 返回要获取的元素组成的数组
 */
function baseAt(object, paths) {
  // 初始化
  let index = -1;
  const length = paths.length;
  const result = new Array(length);
  const skip = object == null;

  // 迭代
  while (++index < length) {
    // object为null或undefined时,就返回undefined
    // 否则就给result对应位置赋值get();
    result[index] = skip ? undefined : get(object, paths[index]);
  }
  return result;
}

export default baseAt;

baseGet

import castPath from './castPath.js';
import toKey from './toKey.js';

/**
 * `get`方法的基础实现,不用支持默认值
 *
 * @private
 * @param {Object} object 要检索的对象。
 * @param {Array|string} path 要获取属性的路径。
 * @returns {*} 返回解析的值。
 */
function baseGet(object, path) {
  // 拿到路径数组
  path = castPath(path, object);

  let index = 0;
  const length = path.length;

  // 迭代,一级级向下拿到最终的值
  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  // index == length,也就是执行到底时,返回最终的值
  return index && index == length ? object : undefined;
}

export default baseGet;

baseIndexOfWith

/**
 * 该函数类似`baseIndexOf`,但是接受一个comparator
 *
 * @private
 * @param {Array} array 要检查的数组
 * @param {*} value 要搜索的值
 * @param {number} fromIndex 开始搜索位置处的索引
 * @param {Function} comparator 比较器comparator调用每个元素
 * @returns {number} 返回匹配值得索引,否则返回`-1`
 */
function baseIndexOfWith(array, value, fromIndex, comparator) {
  // 从设定的索引处开始
  let index = fromIndex - 1;
  // 这里用了解构方式获取length,赞成
  const { length } = array;

  // 开始迭代
  while (++index < length) {
    // 拿array的每一个元素通过comparator的方式与value做对比
    if (comparator(array[index], value)) {
      // 找到了就返回索引
      return index;
    }
  }
  // 找不到就返回-1
  return -1;
}

export default baseIndexOfWith;

baseUnset

import castPath from './castPath.js';
import last from '../last.js';
import parent from './parent.js';
import toKey from './toKey.js';

/**
 * `unset`方法的基础实现
 *
 * @private
 * @param {Object} object 要修改的对象
 * @param {Array|string} path 要移除的属性路径
 * @returns {boolean} 如果删除成功,那么返回 `true` ,否则返回 `false`。
 */
function baseUnset(object, path) {
  // 获取属性路径数组
  path = castPath(path, object);
  // 获取父级属性值的引用
  object = parent(object, path);
  // 在父级属性值中删掉该属性
  return object == null || delete object[toKey(last(path))];
}

export default baseUnset;

castPath

import isKey from './isKey.js';
import stringToPath from './stringToPath.js';

/**
 * 当`value`不是单独值的时候,返回path数组
 *
 * @private
 * @param {*} value 要检查的值
 * @param {Object} [object] 要查询key的对象
 * @returns {Array} 返回转换换后的属性路径数组
 */
function castPath(value, object) {
  // value是数组的时候直接返回value
  if (Array.isArray(value)) {
    return value;
  }
  // 如果value是一个key,就用数组包起来,否则调用stringToPath
  return isKey(value, object) ? [value] : stringToPath(value);
}

export default castPath;

compareAscending

import isSymbol from '../isSymbol.js';

/**
 * 比较value,并用升序排序
 *
 * @private
 * @param {*} value 要比较的值
 * @param {*} other 另一个要比较的值
 * @returns {number} 返回`value`的是否升序排列的指示符
 */
function compareAscending(value, other) {
  // 两个值不完全相等
  if (value !== other) {
    // value是否不为undefined
    const valIsDefined = value !== undefined;
    // value是否为null
    const valIsNull = value === null;
    // value是否是自己本身
    const valIsReflexive = value === value;
    // value是否为symbol
    const valIsSymbol = isSymbol(value);

    // 同上
    const othIsDefined = other !== undefined;
    const othIsNull = other === null;
    const othIsReflexive = other === other;
    const othIsSymbol = isSymbol(other);

    // 如果value为字符串
    const val =
      typeof value === 'string'
        ? // 是就执行字符串比较的结果
          value.localeCompare(other)
        : // 否的话就转为负
          -other;

    // 下面是进行顺序比较
    if (
      (!othIsNull && !othIsSymbol && !valIsSymbol && val > 0) ||
      (valIsSymbol &&
        othIsDefined &&
        othIsReflexive &&
        !othIsNull &&
        !othIsSymbol) ||
      (valIsNull && othIsDefined && othIsReflexive) ||
      (!valIsDefined && othIsReflexive) ||
      !valIsReflexive
    ) {
      return 1;
    }
    if (
      (!valIsNull && !valIsSymbol && !othIsSymbol && val < 0) ||
      (othIsSymbol &&
        valIsDefined &&
        valIsReflexive &&
        !valIsNull &&
        !valIsSymbol) ||
      (othIsNull && valIsDefined && valIsReflexive) ||
      (!othIsDefined && valIsReflexive) ||
      !othIsReflexive
    ) {
      return -1;
    }
  }
  // 如果两个值完全相等===,则返回0
  return 0;
}

export default compareAscending;

copyArrayAt

/**
 * 复制`source`的值到`array`中
 *
 * @private
 * @param {Array} source 复制值的来源
 * @param {Array} [array=[]] 要复制到的数组
 * @returns {Array} 返回 `array`.
 */
function copyArray(source, array) {
  let index = -1;
  const length = source.length;

  // array若是空,就把array设为与source等长的空数组
  array || (array = new Array(length));
  // 开始迭代
  while (++index < length) {
    // 把每个值复制过去,所以是个浅拷贝
    array[index] = source[index];
  }
  return array;
}

export default copyArray;

isKey

import isSymbol from '../isSymbol.js';

/** 用于匹配属性路径中的属性名称。 */
// 带有深度路径属性名的正则
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
// 直接属性名的正则
const reIsPlainProp = /^\w*$/;

/**
 * 检查`value`是一个属性名并且不是一个属性路径
 *
 * @private
 * @param {*} value 要检查的值
 * @param {Object} [object] 要查询key的对象
 * @returns {boolean} 如果时一个属性名则返回`true`,否则返回`false`
 */
function isKey(value, object) {
  // 如果是个数组,则返回false
  if (Array.isArray(value)) {
    return false;
  }
  const type = typeof value;
  // 如果typeof结果是个number、boolean或者value直接为null或者value是个symbol,则返回true
  if (
    type === 'number' ||
    type === 'boolean' ||
    value == null ||
    isSymbol(value)
  ) {
    return true;
  }
  // 直接属性名正则验证 || 带深度的属性名验证 || 或value在object上能查询到
  return (
    reIsPlainProp.test(value) ||
    !reIsDeepProp.test(value) ||
    (object != null && value in Object(object))
  );
}

export default isKey;

memoizeCapped

import memoize from '../memoize.js';

/** 用于memoize缓存的最大条数 */
const MAX_MEMOIZE_SIZE = 500;

/**
 * `memoize`方法的特殊版本,当缓存数超过`MAX_MEMOIZE_SIZE`时会清理memoized
 *
 * @private
 * @param {Function} func 将该函数的输出缓存
 * @returns {Function} 返回一个新的memoized函数
 */
function memoizeCapped(func) {
  // 这里借用resolver函数,计算result.cache的size
  const result = memoize(func, (key) => {
    const { cache } = result;
    // 当size达到300时,清理cache
    if (cache.size === MAX_MEMOIZE_SIZE) {
      cache.clear();
    }
    return key;
  });

  return result;
}

export default memoizeCapped;

parent

import baseGet from './baseGet.js';
import slice from '../slice.js';

/**
 * 获取`object`上`path`的父级属性值
 *
 * @private
 * @param {Object} object 要查询的对象
 * @param {Array} path 要获取父级属性值的路径
 * @returns {*} 返回父级属性值
 */
function parent(object, path) {
  // 当length小于2,也就是length == 1时,直接就返回object
  // 否则,就用baseGet获取(去掉最后一个路径的)路径数组对应的值
  return path.length < 2 ? object : baseGet(object, slice(path, 0, -1));
}

export default parent;

stringToPath

import memoizeCapped from './memoizeCapped.js';

// 点的charCode
const charCodeOfDot = '.'.charCodeAt(0);
// 转义字符正则表达式
const reEscapeChar = /\\(\\)?/g;
// 属性名正则表达式
const rePropName = RegExp(
  // 匹配不是.或括号的任意值
  '[^.[\\]]+' +
    '|' +
    // 或者匹配括号内的属性名
    '\\[(?:' +
    // 匹配一个非字符串表达式
    '([^"\'][^[]*)' +
    '|' +
    // 或者是匹配字符串(支持转义字符)
    '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
    ')\\]' +
    '|' +
    // 或者匹配 "" 作为连续 点 或 空括号之间的空白
    '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))',
  'g'
);

/**
 * 将一个`string`转化为属性路径数组
 *
 * @private
 * @param {string} string 要转化的字符串
 * @returns {Array} 返回属性路径数组
 */
// stringToPath是一个缓存函数
const stringToPath = memoizeCapped((string) => {
  // 定义一个空数组result
  const result = [];
  // 当第一个字符为 点 时,push一个''
  if (string.charCodeAt(0) === charCodeOfDot) {
    result.push('');
  }

  // 借用String.prototype的replace方法
  // 第一个参数是正则表达式,第二个参数是创建新的子字符串的函数
  // match是匹配的子串,,substring是被匹配的字符串
  string.replace(rePropName, (match, expression, quote, subString) => {
    let key = match;
    if (quote) {
      key = subString.replace(reEscapeChar, '$1');
    } else if (expression) {
      key = expression.trim();
    }
    result.push(key);
  });
  // 将result返回
  return result;
});

export default stringToPath;

toKey

import isSymbol from '../isSymbol.js';

/** 作为多种`Number`类型常量的引用 */
const INFINITY = 1 / 0;

/**
 * 当`value`不是string或symbol,将`value`转化为字符串key
 *
 * @private
 * @param {*} value 要检查的值
 * @returns {string|symbol} 返回key
 */
function toKey(value) {
  // 当为string或symbol时,直接返回value
  if (typeof value === 'string' || isSymbol(value)) {
    return value;
  }
  // 当不是string时,先强制转化为字符串
  const result = `${value}`;
  // 当为-0时返回-0,其余直接返回
  return result == '0' && 1 / value == -INFINITY ? '-0' : result;
}

export default toKey;

依赖的 lodash 方法

memoize

/**
 *
 * 创建一个会缓存 `func` 结果的函数。
 * 如果提供了 `resolver` ,就用 `resolver` 的返回值作为 `key` 缓存函数的结果。
 * 默认情况下用第一个参数作为缓存的 `key`。 `func` 在调用时 `this` 会绑定在缓存函数上。
 *
 * **注意:** 缓存会暴露在缓存函数的 `cache`属性上。
 * 它是可以定制的,只要替换了 `memoize.Cache` 构造函数,
 * 或实现了 [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
 * 的 `clear`, `delete`, `get`, `has`, 和 `set`方法。
 *
 * @since 0.1.0
 * @category Function
 * @param {Function} func 需要将输出缓存的函数。
 * @param {Function} [resolver] 这个函数的返回值作为缓存的 key。
 * @returns {Function} 返回新的缓存化后的函数。
 * @example
 *
 * const object = { 'a': 1, 'b': 2 }
 * const other = { 'c': 3, 'd': 4 }
 *
 * const values = memoize(values)
 * values(object)
 * // => [1, 2]
 *
 * values(other)
 * // => [3, 4]
 *
 * object.a = 2
 * values(object)
 * // => [1, 2]
 *
 * // 修改结果缓存
 * values.cache.set(object, ['a', 'b'])
 * values(object)
 * // => ['a', 'b']
 *
 * // 替换 `memoize.Cache`.
 * memoize.Cache = WeakMap
 */
function memoize(func, resolver) {
  // 当func不是函数 || resolver不是函数的时候,报错
  if (
    typeof func !== 'function' ||
    (resolver != null && typeof resolver !== 'function')
  ) {
    throw new TypeError('Expected a function');
  }
  // 定义一个函数memoized
  const memoized = function (...args) {
    // 如果resolver存在,就用resolver转化一下参数作为key,否则直接把第一个参数作为key
    const key = resolver ? resolver.apply(this, args) : args[0];
    const cache = memoized.cache;

    // 当cache中有key的时候,直接返回该key的值。完成了读取缓存的操作
    if (cache.has(key)) {
      return cache.get(key);
    }
    // cache没有这个key的时候,就用func执行这些参数
    const result = func.apply(this, args);
    // 然后在cache设置这个key的值为result,完成了设置缓存操作
    memoized.cache = cache.set(key, result) || cache;
    return result;
  };
  // 设置memoized这个函数对象的cache属性,默认为一个map
  memoized.cache = new (memoize.Cache || Map)();
  // 把memoized函数返回
  return memoized;
}

memoize.Cache = Map;

export default memoize;

get

import baseGet from './.internal/baseGet.js';

/**
 * 根据 `object`对象的`path`路径获取值。 如果解析 `value` 是 `undefined` 会以 `defaultValue` 取代。
 *
 *
 * @since 3.7.0
 * @category Object
 * @param {Object} object 要检索的对象。
 * @param {Array|string} path 要获取属性的路径。
 * @param {*} [defaultValue] 如果解析值是 undefined ,defaultValue会被返回。
 * @returns {*} 返回解析的值。
 * @see has, hasIn, set, unset
 * @example
 *
 * const object = { 'a': [{ 'b': { 'c': 3 } }] }
 *
 * get(object, 'a[0].b.c')
 * // => 3
 *
 * get(object, ['a', '0', 'b', 'c'])
 * // => 3
 *
 * get(object, 'a.b.c', 'default')
 * // => 'default'
 */
function get(object, path, defaultValue) {
  // object为null或undefined时,返回undefined,否则返回baseGet
  const result = object == null ? undefined : baseGet(object, path);
  // result为undefined时,则返回默认值,否则返回result
  return result === undefined ? defaultValue : result;
}

export default get;

核心内置方法

basePullAll

import map from '../map.js';
import baseIndexOf from './baseIndexOf.js';
import baseIndexOfWith from './baseIndexOfWith.js';
import copyArray from './copyArray.js';

/**
 *  `pullAll`系列方法的基础实现
 *
 * @private
 * @param {Array} array 要修改的数组。
 * @param {Array} values 要移除值的数组。
 * @param {Function} [iteratee] iteratee(迭代器)调用每个元素。
 * @param {Function} [comparator] comparator(比较器)调用每个元素。
 * @returns {Array} 返回 `array`.
 */
function basePullAll(array, values, iteratee, comparator) {
  // 使用comparator时用baseIndexOfWith,否则用baseIndexOf
  const indexOf = comparator ? baseIndexOfWith : baseIndexOf;
  // 拿到要移除数组的长度
  const length = values.length;

  let index = -1;
  // 把array的指针赋给seen
  let seen = array;

  // 如果array和values引用的同一个对象
  if (array === values) {
    // 就把values换个地址
    values = copyArray(values);
  }
  // 如果iteratee参数存在,就把seen的所有元素执行下iteratee后的新数组再返回给seen
  if (iteratee) {
    seen = map(array, (value) => iteratee(value));
  }
  // 开始迭代
  while (++index < length) {
    let fromIndex = 0;
    const value = values[index];
    // 当iteratee存在时,则转换下(因为之前把array的转换了,现在需要转换values的),否则返回value
    const computed = iteratee ? iteratee(value) : value;

    // 如果在seen(array)中能找到匹配值的索引
    while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
      // 也就是iteratee参数存在
      if (seen !== array) {
        // 则从seen中删去该值
        seen.splice(fromIndex, 1);
      }
      // array中也删去该值
      array.splice(fromIndex, 1);
    }
    // 内层while执行完后,就决定当前的元素要不要删
  }
  // 外层while执行完后,就决定了array中每一个元素要不要删
  return array;
}

export default basePullAll;

basePullAt

该方法有 bug,本系列全部完成后给 lodash 统一提个 pull request 吧

import baseUnset from './baseUnset.js';
import isIndex from './isIndex.js';

/**
 * `pullAt`方法的基础实现,不支持单独的索引或捕获被移除的元素
 *
 * @private
 * @param {Array} array 要修改的数组
 * @param {number[]} indexes 要删除元素的索引数组
 * @returns {Array} 返回数组
 */
// 注意: 传到indexes中的数组必须已经排序过,不然会出bug
function basePullAt(array, indexes) {
  // 初始化
  let length = array ? indexes.length : 0;
  const lastIndex = length - 1;

  // 开始从右向左循环
  while (length--) {
    // 此处有bug,应该移到while循环外,否则每次的previous都是重新定义的
    let previous;
    // index是当前迭代的索引
    const index = indexes[length];
    // 当length === lastIndex,是让第一次循环能执行
    // 或 index !== previous时,是让后来能运行,因为indexes其实已经排序过了,
    // 所以只用跟前一个元素比较看看有没有重复就可以了,重复就跳过
    if (length === lastIndex || index !== previous) {
      // 让previous等于index
      previous = index;
      // 删除该位置的索引
      if (isIndex(index)) {
        array.splice(index, 1);
      } else {
        baseUnset(array, index);
      }
    }
  }
  return array;
}

export default basePullAt;

对比打包好的 lodash 库中的 basePullAt,其实是掩盖了bug存在的。如果直接执行basePullAt源码,错误就会暴露出来。

function basePullAt(array, indexes) {
  var length = array ? indexes.length : 0,
    lastIndex = length - 1;

  while (length--) {
    var index = indexes[length];
    if (length == lastIndex || index !== previous) {
      // 对比可以发现,此处的previous是使用var定义,并且定义的位置
      // 放在了if判断的后面,导致if判断依然能生效,掩盖了bug
      var previous = index;
      if (isIndex(index)) {
        splice.call(array, index, 1);
      } else {
        baseUnset(array, index);
      }
    }
  }
  return array;
}

pull 家族

pull

import pullAll from './pullAll.js';

/**
 * 移除数组`array`中所有和给定值相等的元素,
 * 使用 [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) 进行全等比较。
 *
 * **注意:** 和 `without` 方法不同,这个方法会改变数组。通过断言使用 `remove` 从一个数组中移除元素。
 *
 * @since 2.0.0
 * @category Array
 * @param {Array} array 要修改的数组。
 * @param {...*} [values] 要删除的值。
 * @returns {Array} 返回 `array`.
 * @see pullAll, pullAllBy, pullAllWith, pullAt, remove, reject
 * @example
 *
 * const array = ['a', 'b', 'c', 'a', 'b', 'c']
 *
 * pull(array, 'a', 'c')
 * console.log(array)
 * // => ['b', 'b']
 */
function pull(array, ...values) {
  // 核心调用的pullAll
  return pullAll(array, values);
}

export default pull;

pullAll

import basePullAll from './.internal/basePullAll.js';

/**
 * 这个方法类似_.pull,区别是这个方法接收一个要移除值的数组。
 *
 *   **注意:** 和 `difference` 方法不同,这个方法会改变数组。
 *
 * @since 4.0.0
 * @category Array
 * @param {Array} array  要修改的数组。
 * @param {Array} values 要移除值的数组。
 * @returns {Array} 返回 `array`.
 * @see pull, pullAllBy, pullAllWith, pullAt, remove, reject
 * @example
 *
 * const array = ['a', 'b', 'c', 'a', 'b', 'c']
 *
 * pullAll(array, ['a', 'c'])
 * console.log(array)
 * // => ['b', 'b']
 */
function pullAll(array, values) {
  // array和value有值 && array和value有长度
  return array != null && array.length && values != null && values.length
    ? // 满足则调用basePullAll
      basePullAll(array, values)
    : array;
}

export default pullAll;

pullAllBy

import basePullAll from './.internal/basePullAll.js';

/**
 * 这个方法类似于 `pullAll` ,区别是这个方法接受一个 `iteratee`(迭代函数)
 * 调用 `array` 和 `values`的每个值以产生一个值,通过产生的值进行了比较。
 * iteratee 会传入一个参数: (value)。
 *
 * **注意:** 不同于 `differenceBy`, 这个方法会改变数组 array。
 *
 * @since 4.0.0
 * @category Array
 * @param {Array} array The array to modify.要修改的数组。
 * @param {Array} values The values to remove.要移除值的数组。
 * @param {Function} iteratee iteratee(迭代器)调用每个元素。
 * @returns {Array} 返回 `array`.
 * @see pull, pullAll, pullAllWith, pullAt, remove, reject
 * @example
 *
 * const array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]
 *
 * pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x')
 * console.log(array)
 * // => [{ 'x': 2 }]
 */
function pullAllBy(array, values, iteratee) {
  // array和value有值 && array和value有长度
  return array != null && array.length && values != null && values.length
    ? // 满足则调用basePullAll,但是多加参数iteratee
      basePullAll(array, values, iteratee)
    : array;
}

export default pullAllBy;

pullAllWith

import basePullAll from './.internal/basePullAll.js';

/**
 *
 * 这个方法类似于 `pullAll` ,区别是这个方法接受 comparator
 * 调用array中的元素和values比较。
 * comparator 会传入两个参数:(arrVal, othVal)。
 *
 * **注意:** 不同于 `differenceWith`, 这个方法会改变数组 array。
 *
 * @since 4.6.0
 * @category Array
 * @param {Array} array 要修改的数组。
 * @param {Array} values 要移除值的数组。
 * @param {Function} [comparator] comparator(比较器)调用每个元素。
 * @returns {Array} 返回 `array`.
 * @see pull, pullAll, pullAllBy, pullAt, remove, reject
 * @example
 *
 * const array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]
 *
 * pullAllWith(array, [{ 'x': 3, 'y': 4 }], isEqual)
 * console.log(array)
 * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]
 */
function pullAllWith(array, values, comparator) {
  // array和value有值 && array和value有长度
  return array != null && array.length && values != null && values.length
    ? // 满足则调用basePullAll,但是多加参数comparator
      basePullAll(array, values, undefined, comparator)
    : array;
}

export default pullAllWith;

pullAt

import map from './map.js';
import baseAt from './.internal/baseAt.js';
import basePullAt from './.internal/basePullAt.js';
import compareAscending from './.internal/compareAscending.js';
import isIndex from './.internal/isIndex.js';

/**
 * 根据索引 `indexes`,移除`array`中对应的元素,并返回被移除元素的数组。
 *
 * **注意:** 和 `at`不同, 这个方法会改变数组 `array`。
 *
 *
 * @since 3.0.0
 * @category Array
 * @param {Array} array 要修改的数组。
 * @param {...(number|number[])} [indexes] 要移除元素的索引。
 * @returns {Array} 返回移除元素组成的新数组。
 * @see pull, pullAll, pullAllBy, pullAllWith, remove, reject
 * @example
 *
 * const array = ['a', 'b', 'c', 'd']
 * const pulled = pullAt(array, [1, 3])
 *
 * console.log(array)
 * // => ['a', 'c']
 *
 * console.log(pulled)
 * // => ['b', 'd']
 */
function pullAt(array, ...indexes) {
  // 取到length
  const length = array == null ? 0 : array.length;

  const result = baseAt(array, indexes);

  basePullAt(
    array,
    map(indexes, (index) => (isIndex(index, length) ? +index : index)).sort(
      compareAscending
    )
  );
  return result;
}

export default pullAt;

原生实现

pull方法的原生实现如下:

// 原生实现
function pull(arr, ...removeList) {
  // 使用set首先能对removeList去重
  // 第二set自带增删查改等各种方法
  let removeSet = new Set(removeList);
  // 直接对arr过滤,如果set中有该元素,就过滤掉。
  return arr.filter((el) => {
    return !removeSet.has(el);
  });
}
let array = [1, 2, 3, 1, 2, 3];
console.log(pull(array, 2, 3));
// [1, 1]