lodash里的maxBy和minBy

447 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

使用说明

maxBy

lodash里的maxBy方法类似max方法,它接受在迭代的时候调用第二个参数方法,来生成其值排序的标准。

参数说明:

  • 参数一是数组,默认是一个对象数组,每一项都是对象。

  • 参数二是迭代对比的规则。

    当参数二是函数时,return的是对比的标准,该迭代方法的参数是数组的项。

    当参数二是字符串时,则代表数组项里的属性。

var objects = [{ 'n': 1 }, { 'n': 2 }];
 
_.maxBy(objects, function(o) { return o.n; });
// => { 'n': 2 }

_.maxBy(objects, 'n');
// => { 'n': 2 }

从说明中我们明白了其实现是基于之前篇章里的讲到的max方法,在max方法求最大值的过程中,即在每一次遍历中调用第二个参数,参数为数组的项。

minBy

minBy同maxBy方法一致,目的在于求最小的那一项。

var objects = [{ 'n'1 }, { 'n'2 }];
 
_.minBy(objects, function(o) { return o.n; });
// => { 'n': 1 }
 
// The `_.property` iteratee shorthand.
_.minBy(objects, 'n');
// => { 'n': 1 }

手写实现

maxBy

手写maxBy方法,考虑到该方法的第二项可以传递两种类型,所以我们可以做如下封装:

function maxBy(array, iteratee) {
    if (!Array.isArray(array)) return undefined
    if (typeof iteratee !== 'function' && typeof iteratee !== "string") return array[0]

    const length = array.length;
    let maxValue = null
    let maxIndex = 0
    let handle = null
    
    if (typeof iteratee === "function") {
        handle = item => iteratee(item)
        maxValue = iteratee(array[0])
    } else {
        handle = item => item[iteratee]
        maxValue = array[0][iteratee]
    }

    for (let i = 1; i < length; i++) {
        if (maxValue < handle(array[i])) {
            maxValue = handle(array[i])
            maxIndex = i
        }
    }
    return array[maxIndex]
}

我们第一步先进行严谨性判断,当出现特例情况时对其进行指定数据的返回。

当参数满足要求后,我们对第二个参数进行判断,当该参数是字符串时选择读取,当该参数是函数时选择调用。

当封装好handle方法之后,我们则根据max的实现思路进行遍历、对比、存储,最后返回指定的项。

minBy

minBy同maxBy的区别在于比较时的基准,更改比较符号即可。

function minBy(array, iteratee) {
    if (!Array.isArray(array)) return undefined
    if (typeof iteratee !== 'function' && typeof iteratee !== "string") return array[0]

    const length = array.length;
    let minValue = null
    let minIndex = 0
    let handle = null
    
    if (typeof iteratee === "function") {
        handle = item => iteratee(item)
        minValue = iteratee(array[0])
    } else {
        handle = item => item[iteratee]
        minValue = array[0][iteratee]
    }

    for (let i = 1; i < length; i++) {
        if (minValue > handle(array[i])) {
            minValue = handle(array[i])
            minIndex = i
        }
    }
    return array[minIndex]
}

源码实现

maxBy

查看lodash里的maxBy,我们看到当第一个参数只有存在并且身上具有length属性时才会调用baseExtremum方法,否则返回undefined。

function maxBy(array, iteratee) {
  return (array && array.length)
    ? baseExtremum(array, baseIteratee(iteratee, 2), baseGt)
    : undefined;
}

在之前的篇章中我们求极值便是用到了baseExtremum方法。而该方法正是对一组可循环操作的数据进行遍历、对比、存储。

function baseExtremum(array, iteratee, comparator) {
  var index = -1,
      length = array.length;

  while (++index < length) {
    var value = array[index],
        current = iteratee(value);

    if (current != null && (computed === undefined
          ? (current === current && !isSymbol(current))
          : comparator(current, computed)
        )) {
      var computed = current,
          result = value;
    }
  }
  return result;
}

baseGt代码如下,主要是判断第一个值是否大于第二个值。

function baseGt(value, other) {
  return value > other;
}

对比求极值,我们更加关注的是每一次对比的过程,以及如何对比。

对于maxBy,我们更加关注的是第二个参数的使用。

在lodash里,对比操作被封装到一个baseIteratee的方法里,因为我们并不清楚maxBy第二个参数传入的类型,这部分实现类似于我们上面实现的maxBy方法里对第二个参数的判断过程。

function baseIteratee(value) {
  if (typeof value == 'function') {
    return value;
  }
  if (value == null) {
    return identity;
  }
  if (typeof value == 'object') {
    return isArray(value)
      ? baseMatchesProperty(value[0], value[1])
      : baseMatches(value);
  }
  return property(value);
}

其中baseIteratee运用了下面封装的工具方法:

// 判断数组
var isArray = Array.isArray;

// 获取赋值
function identity(value) {
  return value;
}

关于对象和数组的判断以及后续处理逻辑,在maxBy中暂时不需要,因为封装的baseIteratee在后续需要迭代处理的方法中需要用到。

关于property方法,对于maxBy第二个参数为字符串类型的情况则可调用该方法,该方法判断是否符合对象key值,即基本数据类型。

function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}

function toKey(value) {
  if (typeof value == 'string' || isSymbol(value)) {
    return value;
  }
  var INFINITY = 1 / 0;
  var result = (value + '');
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}

function isKey(value, object) {
  if (isArray(value)) {
    return false;
  }
  var reIsDeepProp = /.|[(?:[^[]]*|(["'])(?:(?!\1)[^\]|\.)*?\1)]/,
    reIsPlainProp = /^\w*$/;
  var type = typeof value;
  if (type == 'number' || type == 'symbol' || type == 'boolean' ||
      value == null || isSymbol(value)) {
    return true;
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object));
}

maxBy第二个参数字符串类型下则调用baseProperty方法,该方法返回一个函数,该函数接受的对象类型,即在maxBy里遍历时传入的数组项,所以maxBy第二个参数为字符串类型下,实际在迭代处理时字符串被处理成函数方法,迭代处理时仍调用的是函数。

function baseProperty(key) {
  return function(object) {
    return object == null ? undefined : object[key];
  };
}

minBy

同理与maxBy的唯一区别则是在对比时的符号,换个方向就行了,maxBy内部在调用baseExtremum时传入baseGT,在这里我们传入baseLt即可。

function minBy(array, iteratee) {
  return (array && array.length)
    ? baseExtremum(array, baseIteratee(iteratee, 2), baseLt)
    : undefined;
}

function baseLt(value, other) {
  return value < other;
}

小结

maxBy和minBy同max和min方法实现思路一致,并且在迭代数组的时候都用到了baseExtremum方法。而baseExtremum只管迭代数组并调用第二个参数,所以第二个参数为函数类型。

对于maxBy和minBy,关键则在于对各自身上的第二个参数类型进行转换处理,字符串类型最终会转换成函数类型给迭代时调用。

这也是identity方法封装的必要性,因为传入的参数并不清楚类型,兼容性处理则可以在后面的方法中进行增强。如果当初在遍历数组的时候直接对current进行赋值操作而非函数调用,那就没办法使用增强功能了,即字符串处理成函数的情况无法实现。

所以封装并非一簇而成的,而是在发现通用性的基础上思考可拓展性。