This article analyzes the Pull family of methods, including pull, pullAll, pullAllBy, pullAllWith, pullAt and the core methods basePullAll, basePullAt. By the way, the lodash method memoize is analyzed and the native implementation of the pull method is presented.

In addition, I found a bug in the source code of the basePullAt method. If the index of the same element to be deleted is listed multiple times in the array, the result of the method execution is not correct, and too many elements are deleted. However, in the packaged file, let was changed to VAR and moved to hide the bug.

Dependent built-in methods

baseAt

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

/** * the basic implementation of the at method, without supporting a single path **@private
 * @param {Object} Object The object to iterate over *@param {string[]} Paths Array of the element paths of the object to obtain@returns {Array} Returns an array of elements to fetch */
function baseAt(object, paths) {
  / / initialization
  let index = -1;
  const length = paths.length;
  const result = new Array(length);
  const skip = object == null;

  / / iteration
  while (++index < length) {
    // If object is null or undefined, undefined is returned
    // Get ();
    result[index] = skip ? undefined : get(object, paths[index]);
  }
  return result;
}

export default baseAt;
Copy the code

baseGet

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

/** * the basic implementation of the 'get' method, does not support the default value **@private
 * @param {Object} Object Indicates the object to be retrieved. *@param {Array|string} Path Path to obtain the attribute. *@returns {*} Returns the parsed value. * /
function baseGet(object, path) {
  // Get the path array
  path = castPath(path, object);

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

  // Iterate one level down to get the final value
  while(object ! =null && index < length) {
    object = object[toKey(path[index++])];
  }
  // index == length, which returns the final value when executed
  return index && index == length ? object : undefined;
}

export default baseGet;
Copy the code

baseIndexOfWith

/** * This function is similar to 'baseIndexOf' but accepts a comparator **@private
 * @param {Array} Array Array to check *@param {*} Value Indicates the value to be searched *@param {number} FromIndex starts searching the index * at the location@param {Function} The comparator comparator calls each element *@returns {number} Returns the matching value index, otherwise returns' -1 '*/
function baseIndexOfWith(array, value, fromIndex, comparator) {
  // Start at the specified index
  let index = fromIndex - 1;
  // Get the length with the destruct method
  const { length } = array;

  // Start the iteration
  while (++index < length) {
    // Compare each element of array to value using the comparator
    if (comparator(array[index], value)) {
      // Return index if found
      returnindex; }}// Return -1 if not found
  return -1;
}

export default baseIndexOfWith;
Copy the code

baseUnset

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

/** * the basic implementation of the 'unset' method **@private
 * @param {Object} Object Indicates the object * to be modified@param {Array|string} Path Path of the property to be removed *@returns {boolean} Return 'true' if the deletion succeeded, false otherwise. * /
function baseUnset(object, path) {
  // Get the property path array
  path = castPath(path, object);
  // Get a reference to the parent attribute value
  object = parent(object, path);
  // Delete the parent attribute value
  return object == null || delete object[toKey(last(path))];
}

export default baseUnset;
Copy the code

castPath

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

/** * Returns the path array ** when 'value' is not a single value@private
 * @param {*} Value Indicates the value to be checked *@param {Object} [object] The object whose key is to be queried *@returns {Array} Returns the transformed property path array */
function castPath(value, object) {
  // return value when value is an array
  if (Array.isArray(value)) {
    return value;
  }
  // If value is a key, wrap it in an array, otherwise call stringToPath
  return isKey(value, object) ? [value] : stringToPath(value);
}

export default castPath;
Copy the code

compareAscending

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

/** * compares values and sorts ** in ascending order@private
 * @param {*} Value Specifies the value to be compared *@param {*} Other Another value to compare *@returns {number} Returns an indicator of whether 'value' is in ascending order */
function compareAscending(value, other) {
  // The two values are not exactly equal
  if(value ! == other) {// value specifies whether the value is not undefined
    constvalIsDefined = value ! = =undefined;
    // value specifies whether the value is null
    const valIsNull = value === null;
    // value is itself
    const valIsReflexive = value === value;
    // whether value is symbol
    const valIsSymbol = isSymbol(value);

    / / same as above
    constothIsDefined = other ! = =undefined;
    const othIsNull = other === null;
    const othIsReflexive = other === other;
    const othIsSymbol = isSymbol(other);

    // If value is a string
    const val =
      typeof value === 'string'
        ? // is the result of performing a string comparison
          value.localeCompare(other)
        : // If not, it becomes negative
          -other;

    // Make a sequential comparison
    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; }}// If two values are exactly equal ===, 0 is returned
  return 0;
}

export default compareAscending;
Copy the code

copyArrayAt

/** * copy the value of 'source' to 'array' **@private
 * @param {Array} Source Source of the replicated value *@param {Array} [array=[]] Array * to copy to@returns {Array} Return ` array `. * /
function copyArray(source, array) {
  let index = -1;
  const length = source.length;

  // If array is empty, set array to be the same length as source
  array || (array = new Array(length));
  // Start the iteration
  while (++index < length) {
    // Copy each value, so it's a shallow copy
    array[index] = source[index];
  }
  return array;
}

export default copyArray;
Copy the code

isKey

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

/** is used to match the attribute name in the attribute path. * /
// Regex with depth path attribute name
const reIsDeepProp = 1 / \ | \ [(? : [^ [\]] * | (["]) (? : (?! \ 1) | \ \ [^ \ \]) *? \ \] / 1);
// Regex for direct attribute names
const reIsPlainProp = /^\w*$/;

/** * Check that 'value' is an attribute name and not an attribute path **@private
 * @param {*} Value Indicates the value to be checked *@param {Object} [object] The object whose key is to be queried *@returns {boolean} Returns' true 'if an attribute name is used,' false '*/ otherwise
function isKey(value, object) {
  // Return false if it is an array
  if (Array.isArray(value)) {
    return false;
  }
  const type = typeof value;
  Return true if typeof is a number, Boolean, or value is directly null or value is a symbol
  if (
    type === 'number' ||
    type === 'boolean' ||
    value == null ||
    isSymbol(value)
  ) {
    return true;
  }
  Directly attribute name / / regular verification | | with depth of the property name validation | | or value can query on the object
  return( reIsPlainProp.test(value) || ! reIsDeepProp.test(value) || (object ! =null && value in Object(object))
  );
}

export default isKey;
Copy the code

memoizeCapped

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

/** Maximum number of bytes for memoize cache */
const MAX_MEMOIZE_SIZE = 500;

/** * a special version of the 'memoize' method that cleans memoized ** when the number of caches exceeds' MAX_MEMOIZE_SIZE '@private
 * @param {Function} Func caches the output of this function *@returns {Function} Returns a new memoized function */
function memoizeCapped(func) {
  Result. cache = result.cache ()
  const result = memoize(func, (key) = > {
    const { cache } = result;
    // Clear the cache when the size reaches 300
    if (cache.size === MAX_MEMOIZE_SIZE) {
      cache.clear();
    }
    return key;
  });

  return result;
}

export default memoizeCapped;
Copy the code

parent

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

/** * Gets the parent property of path on object **@private
 * @param {Object} Object Indicates the object to be queried@param {Array} Path Path to obtain the value of the parent attribute *@returns {*} Returns the parent property value */
function parent(object, path) {
  // If length is less than 2 (length == 1), return object
  Otherwise, baseGet is used to get the corresponding value of the path array (excluding the last path)
  return path.length < 2 ? object : baseGet(object, slice(path, 0, -1));
}

export default parent;
Copy the code

stringToPath

import memoizeCapped from './memoizeCapped.js';

/ / charCode
const charCodeOfDot = '. '.charCodeAt(0);
// Escape character regular expressions
const reEscapeChar = / \ \ (\ \)? /g;
// Attribute name regular expression
const rePropName = RegExp(
  // The match is not. Or any value of the parenthesis
  '[^. [\ \]] +' +
    '|' +
    // Or match the attribute name in parentheses
    '\ \ [(? : +
    // Matches a non-string expression
    '([[^ ^ "\'] [] *) ' +
    '|' +
    // Or match strings (escape characters are supported)
    '([" \]) ((? : (? ! \ \ [^ \ \ \ \] 2) | \ \ \ \.) *?) \ \ 2 ' +
    ') \ \] ' +
    '|' +
    // Or match "" as a continuous point or space between empty parentheses
    '(? = (? : \ \ | \ \ [\ \])? : \ \ | \ \ / \ \ | $)) '.'g'
);

/** * Convert a string to an attribute path array **@private
 * @param {string} String String to be converted *@returns {Array} Returns the property path array */
// stringToPath is a cache function
const stringToPath = memoizeCapped((string) = > {
  // Define an empty array result
  const result = [];
  // When the first character is a dot, push a ''
  if (string.charCodeAt(0) === charCodeOfDot) {
    result.push(' ');
  }

  // Borrow the string. prototype replace method
  // The first argument is a regular expression, and the second argument is a function that creates a new substring
  // match is the matched substring, and substring is the matched string
  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);
  });
  // Return result
  return result;
});

export default stringToPath;
Copy the code

toKey

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

/** references multiple 'Number' type constants */
const INFINITY = 1 / 0;

/** * Convert 'value' to the string key ** when 'value' is not a string or symbol@private
 * @param {*} Value Indicates the value to be checked *@returns {string|symbol} Returns the key * /
function toKey(value) {
  // When it is string or symbol, value is returned
  if (typeof value === 'string' || isSymbol(value)) {
    return value;
  }
  // If it is not a string, force it to be a string
  const result = `${value}`;
  // Return -0 if -0, otherwise return directly
  return result == '0' && 1 / value == -INFINITY ? '0' : result;
}

export default toKey;
Copy the code

Dependent lodash methods

memoize

/** ** Create a function that will cache the results of 'func'. * If 'resolver' is provided, the return value of 'resolver' is used as the result of the 'key' cache function. * By default, the first parameter is used as the cache 'key'. This is bound to the cache function when func is called. ** ** Note :** caches are exposed to the 'cache' attribute of the cache function. * It is customizable by replacing the 'memoize.cache' constructor, * or implement 'clear' of [' Map '](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) *, 'delete', 'get', 'has', and' set 'methods. * *@since 0.1.0 from *@category Function
 * @param {Function} Func a function that needs to be cached for output. *@param {Function} [resolver] The return value of this function is used as the cache key. *@returns {Function} Returns the new cached 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] 'b']) * values (object) * / / = > [' a ', 'b'] * * / / replace ` memoize. Cache `. * memoize Cache = WeakMap * /
function memoize(func, resolver) {
  / / when func not function | | resolver is not a function of time, an error
  if (
    typeoffunc ! = ='function'|| (resolver ! =null && typeofresolver ! = ='function')) {throw new TypeError('Expected a function');
  }
  // Define a function memoized
  const memoized = function (. args) {
    // If resolver exists, use resolver as the key; otherwise, use the first parameter as the key
    const key = resolver ? resolver.apply(this, args) : args[0];
    const cache = memoized.cache;

    // If there is a key in the cache, the value of the key is returned. The cache is read
    if (cache.has(key)) {
      return cache.get(key);
    }
    // When the cache does not have this key, it uses func to execute these arguments
    const result = func.apply(this, args);
    // Set this key to result in the cache
    memoized.cache = cache.set(key, result) || cache;
    return result;
  };
  // Set memoized to the cache property of this function object, which is a map by default
  memoized.cache = new (memoize.Cache || Map) ();// Return the memoized function
  return memoized;
}

memoize.Cache = Map;

export default memoize;
Copy the code

get

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

/** * Get the value based on the 'path' of the 'object'. If 'value' is' undefined ', it will be replaced by 'defaultValue'. * * *@since 3.7.0
 * @category Object
 * @param {Object} Object Indicates the object to be retrieved. *@param {Array|string} Path Path to obtain the attribute. *@param {*} [defaultValue] If the resolved value is undefined, defaultValue is returned. *@returns {*} Returns the parsed value. *@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) {
  // If object is null or undefined, undefined is returned; otherwise baseGet is returned
  const result = object == null ? undefined : baseGet(object, path);
  // if result is undefined, the default value is returned; otherwise, result is returned
  return result === undefined ? defaultValue : result;
}

export default get;
Copy the code

Core built-in methods

basePullAll

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

/** * the base implementation of the pullAll series methods **@private
 * @param {Array} Array Array to be modified. *@param {Array} Values The array of values to remove. *@param {Function} Iteratee [iteratee] Iteratee calls each element. *@param {Function} [comparator] The comparator calls each element. *@returns {Array} Return ` array `. * /
function basePullAll(array, values, iteratee, comparator) {
  // baseIndexOfWith when using the comparator, baseIndexOf otherwise
  const indexOf = comparator ? baseIndexOfWith : baseIndexOf;
  // Get the length of the array to remove
  const length = values.length;

  let index = -1;
  // Assign the pointer to array to seen
  let seen = array;

  // If array and values reference the same object
  if (array === values) {
    // change the address of values
    values = copyArray(values);
  }
  If the iteratee argument exists, the new array of seen elements is returned to Seen after iteratee is executed
  if (iteratee) {
    seen = map(array, (value) = > iteratee(value));
  }
  // Start the iteration
  while (++index < length) {
    let fromIndex = 0;
    const value = values[index];
    // If iteratee exists, it will be converted (array, values), otherwise value will be returned
    const computed = iteratee ? iteratee(value) : value;

    // If the index of seen (array) can be found
    while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
      // The iteratee argument exists
      if(seen ! == array) {// Delete the value from seen
        seen.splice(fromIndex, 1);
      }
      // Delete the value from array
      array.splice(fromIndex, 1);
    }
    // The inner while determines whether the current element should be deleted
  }
  // The while determines whether each element in the array should be deleted
  return array;
}

export default basePullAll;
Copy the code

basePullAt

This method is buggy, so make a pull Request for LoDash after this series is complete

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

/** * the base implementation of the 'pullAt' method does not support separate indexes or capture of removed elements **@private
 * @param {Array} Array Array to be modified *@param {number[]} Indexes The index array * of the element to delete@returns {Array} Return the array */
// Note: The array passed to the indexes must have been sorted, otherwise a bug will occur
function basePullAt(array, indexes) {
  / / initialization
  let length = array ? indexes.length : 0;
  const lastIndex = length - 1;

  // Start looping from right to left
  while (length--) {
    // There is a bug here and it should be moved out of the while loop, otherwise each previous is redefined
    let previous;
    // index is the index of the current iteration
    const index = indexes[length];
    // When length === lastIndex, the first loop can be executed
    / / or index! == previous, because the indexes have already been sorted,
    // Just use the previous element to see if there is any repetition
    if(length === lastIndex || index ! == previous) {// Set previous to index
      previous = index;
      // Drop the index at that location
      if (isIndex(index)) {
        array.splice(index, 1);
      } else{ baseUnset(array, index); }}}return array;
}

export default basePullAt;
Copy the code

BasePullAt, in contrast to the packaged LoDash library, hides the bug. If you execute the basePullAt source code directly, the error is exposed.

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 is defined using var and the position of the definition
      // Put it after the if judgment, so that the if judgment still works, hiding the bug
      var previous = index;
      if (isIndex(index)) {
        splice.call(array, index, 1);
      } else{ baseUnset(array, index); }}}return array;
}
Copy the code

The family of the pull

pull

import pullAll from './pullAll.js';

/** * Remove all elements from array that are equal to the given value. * Congruent comparison using [' SameValueZero '](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero). ** ** Note: ** differs from the 'without' method, which changes the array. Remove elements from an array using 'remove' assertion. * *@since 2.0.0
 * @category Array
 * @param {Array} Array Array to be modified. *@param {... *} [values] Indicates the value to be deleted. *@returns {Array} Return ` 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) {
  // Core call pullAll
  return pullAll(array, values);
}

export default pull;
Copy the code

pullAll

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

/** * This method is similar to _. Pull except that it receives an array of values to remove. ** ** Note: ** differs from the 'difference' method, which changes the array. * *@since 4.0.0
 * @category Array
 * @param {Array} Array Array to be modified. *@param {Array} Values The array of values to remove. *@returns {Array} Return ` 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 and value have values && array and value have lengths
  returnarray ! =null&& array.length && values ! =null && values.length
    ? // 满足则调用basePullAll
      basePullAll(array, values)
    : array;
}

export default pullAll;
Copy the code

pullAllBy

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

/** * This method is similar to 'pullAll', except that it takes an 'iteratee' * to call each value of 'array' and 'values' to produce a value, which is then compared. * Iteratee takes a single argument: (value). ** ** Note: unlike 'differenceBy', this method changes the array 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 calls each element. *@returns {Array} Return ` 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 and value have values && array and value have lengths
  returnarray ! =null&& array.length && values ! =null && values.length
    ? // Call basePullAll with iteratee
      basePullAll(array, values, iteratee)
    : array;
}

export default pullAllBy;
Copy the code

pullAllWith

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

/** ** this method is similar to 'pullAll' except that it accepts a comparator * call to array to compare elements and values. * the comparator passes two arguments :(arrVal, othVal). ** ** Note: unlike 'differenceWith', this method changes the array array. * *@since 4.6.0
 * @category Array
 * @param {Array} Array Array to be modified. *@param {Array} Values The array of values to remove. *@param {Function} [comparator] The comparator calls each element. *@returns {Array} Return ` 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 and value have values && array and value have lengths
  returnarray ! =null&& array.length && values ! =null && values.length
    ? // Call basePullAll with the extra parameter comparator
      basePullAll(array, values, undefined, comparator)
    : array;
}

export default pullAllWith;
Copy the code

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';

/** * Returns an array of removed elements based on the indexes. ** ** Note :**, unlike 'at', changes the array. * * *@since 3.0.0
 * @category Array
 * @param {Array} Array Array to be modified. *@param {... (number|number[])} [indexes] The index to remove the element. *@returns {Array} Returns a new array of removed elements. *@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) {
  / / get the 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;
Copy the code

A freestanding implementation

The native implementation of the pull method is as follows:

// Native implementation
function pull(arr, ... removeList) {
  // Use set first to remove the removeList
  // add, delete, change, etc
  let removeSet = new Set(removeList);
  // Filter arr directly. If the element is present in set, filter it out.
  return arr.filter((el) = > {
    return! removeSet.has(el); }); }let array = [1.2.3.1.2.3];
console.log(pull(array, 2.3));
/ / [1, 1)
Copy the code