本篇分析下pull
家族的方法,包括pull
、pullAll
、pullAllBy
、pullAllWith
、pullAt
以及核心方法basePullAll
、basePullAt
。顺便分析依赖到的 lodash 方法memoize
,最后给出pull
方法的原生实现。
具体的依赖路径图如下所示:

另外,在 basePullAt
方法的源码中发现一个 bug
,在数组中多次列出同一个要删除的元素索引的情况下,会导致方法执行结果不对,删除元素过多。但是在打包好文件中将 let
变为了 var
并挪动了位置,掩盖了这个 bug
。
依赖的内置方法
baseAt
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
| 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
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
| import castPath from './castPath.js'; import toKey from './toKey.js';
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++])]; } return index && index == length ? object : undefined; }
export default baseGet;
|
baseIndexOfWith
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
|
function baseIndexOfWith(array, value, fromIndex, comparator) { let index = fromIndex - 1; const { length } = array;
while (++index < length) { if (comparator(array[index], value)) { return index; } } return -1; }
export default baseIndexOfWith;
|
baseUnset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import castPath from './castPath.js'; import last from '../last.js'; import parent from './parent.js'; import toKey from './toKey.js';
function baseUnset(object, path) { path = castPath(path, object); object = parent(object, path); return object == null || delete object[toKey(last(path))]; }
export default baseUnset;
|
castPath
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import isKey from './isKey.js'; import stringToPath from './stringToPath.js';
function castPath(value, object) { if (Array.isArray(value)) { return value; } return isKey(value, object) ? [value] : stringToPath(value); }
export default castPath;
|
compareAscending
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
| import isSymbol from '../isSymbol.js';
function compareAscending(value, other) { if (value !== other) { const valIsDefined = value !== undefined; const valIsNull = value === null; const valIsReflexive = value === value; const valIsSymbol = isSymbol(value);
const othIsDefined = other !== undefined; const othIsNull = other === null; const othIsReflexive = other === other; const othIsSymbol = isSymbol(other);
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; } } return 0; }
export default compareAscending;
|
copyArrayAt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
function copyArray(source, array) { let index = -1; const length = source.length;
array || (array = new Array(length)); while (++index < length) { array[index] = source[index]; } return array; }
export default copyArray;
|
isKey
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
| import isSymbol from '../isSymbol.js';
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
const reIsPlainProp = /^\w*$/;
function isKey(value, object) { if (Array.isArray(value)) { return false; } const type = typeof value; if ( type === 'number' || type === 'boolean' || value == null || isSymbol(value) ) { return true; } return ( reIsPlainProp.test(value) || !reIsDeepProp.test(value) || (object != null && value in Object(object)) ); }
export default isKey;
|
memoizeCapped
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
| import memoize from '../memoize.js';
const MAX_MEMOIZE_SIZE = 500;
function memoizeCapped(func) { const result = memoize(func, (key) => { const { cache } = result; if (cache.size === MAX_MEMOIZE_SIZE) { cache.clear(); } return key; });
return result; }
export default memoizeCapped;
|
parent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import baseGet from './baseGet.js'; import slice from '../slice.js';
function parent(object, path) { return path.length < 2 ? object : baseGet(object, slice(path, 0, -1)); }
export default parent;
|
stringToPath
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
| import memoizeCapped from './memoizeCapped.js';
const charCodeOfDot = '.'.charCodeAt(0);
const reEscapeChar = /\\(\\)?/g;
const rePropName = RegExp( '[^.[\\]]+' + '|' + '\\[(?:' + '([^"\'][^[]*)' + '|' + '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' + ')\\]' + '|' + '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))', 'g' );
const stringToPath = memoizeCapped((string) => { const result = []; if (string.charCodeAt(0) === charCodeOfDot) { result.push(''); }
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; });
export default stringToPath;
|
toKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import isSymbol from '../isSymbol.js';
const INFINITY = 1 / 0;
function toKey(value) { if (typeof value === 'string' || isSymbol(value)) { return value; } const result = `${value}`; return result == '0' && 1 / value == -INFINITY ? '-0' : result; }
export default toKey;
|
依赖的 lodash 方法
memoize
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
|
function memoize(func, resolver) { if ( typeof func !== 'function' || (resolver != null && typeof resolver !== 'function') ) { throw new TypeError('Expected a function'); } const memoized = function (...args) { const key = resolver ? resolver.apply(this, args) : args[0]; const cache = memoized.cache;
if (cache.has(key)) { return cache.get(key); } const result = func.apply(this, args); memoized.cache = cache.set(key, result) || cache; return result; }; memoized.cache = new (memoize.Cache || Map)(); return memoized; }
memoize.Cache = Map;
export default memoize;
|
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
| import baseGet from './.internal/baseGet.js';
function get(object, path, defaultValue) { const result = object == null ? undefined : baseGet(object, path); return result === undefined ? defaultValue : result; }
export default get;
|
核心内置方法
basePullAll
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
| import map from '../map.js'; import baseIndexOf from './baseIndexOf.js'; import baseIndexOfWith from './baseIndexOfWith.js'; import copyArray from './copyArray.js';
function basePullAll(array, values, iteratee, comparator) { const indexOf = comparator ? baseIndexOfWith : baseIndexOf; const length = values.length;
let index = -1; let seen = array;
if (array === values) { values = copyArray(values); } if (iteratee) { seen = map(array, (value) => iteratee(value)); } while (++index < length) { let fromIndex = 0; const value = values[index]; const computed = iteratee ? iteratee(value) : value;
while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { if (seen !== array) { seen.splice(fromIndex, 1); } array.splice(fromIndex, 1); } } return array; }
export default basePullAll;
|
basePullAt
该方法有 bug
,本系列全部完成后给 lodash 统一提个 pull request 吧
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
| import baseUnset from './baseUnset.js'; import isIndex from './isIndex.js';
function basePullAt(array, indexes) { let length = array ? indexes.length : 0; const lastIndex = length - 1;
while (length--) { let previous; const index = indexes[length]; if (length === lastIndex || index !== previous) { previous = index; if (isIndex(index)) { array.splice(index, 1); } else { baseUnset(array, index); } } } return array; }
export default basePullAt;
|
对比打包好的 lodash 库中的 basePullAt
,其实是掩盖了bug存在的。如果直接执行basePullAt
源码,错误就会暴露出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function basePullAt(array, indexes) { var length = array ? indexes.length : 0, lastIndex = length - 1;
while (length--) { var index = indexes[length]; if (length == lastIndex || index !== previous) { var previous = index; if (isIndex(index)) { splice.call(array, index, 1); } else { baseUnset(array, index); } } } return array; }
|
pull 家族
pull
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
| import pullAll from './pullAll.js';
function pull(array, ...values) { return pullAll(array, values); }
export default pull;
|
pullAll
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 basePullAll from './.internal/basePullAll.js';
function pullAll(array, values) { return array != null && array.length && values != null && values.length ? basePullAll(array, values) : array; }
export default pullAll;
|
pullAllBy
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
| import basePullAll from './.internal/basePullAll.js';
function pullAllBy(array, values, iteratee) { return array != null && array.length && values != null && values.length ? basePullAll(array, values, iteratee) : array; }
export default pullAllBy;
|
pullAllWith
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
| import basePullAll from './.internal/basePullAll.js';
function pullAllWith(array, values, comparator) { return array != null && array.length && values != null && values.length ? basePullAll(array, values, undefined, comparator) : array; }
export default pullAllWith;
|
pullAt
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
| 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';
function pullAt(array, ...indexes) { 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
方法的原生实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function pull(arr, ...removeList) { let removeSet = new Set(removeList); return arr.filter((el) => { return !removeSet.has(el); }); } let array = [1, 2, 3, 1, 2, 3]; console.log(pull(array, 2, 3));
|