首发于小正说事
Underscore源码分析系列(2)

Underscore源码分析系列(2)

目录

Underscore源码分析系列(1) - Liril的文章 - 知乎专栏

Underscore源码分析系列(2) - Liril的文章 - 知乎专栏

Underscore源码分析系列(3) - Liril的文章 - 知乎专栏

Underscore源码分析系列(4) - Liril的文章 - 知乎专栏

Underscore源码分析系列(5) - Liril的文章 - 知乎专栏

Underscore源码分析系列(6) - Liril的文章 - 知乎专栏



说明

这一部分是对集合函数进行分析。

_.each、_.forEach

each是所有集合函数的基础,也被叫做forEach,它会对集合内的元素依次使用用户提供的回调函数进行处理,然后返回原集合。其中对于类数组对象将根据索引依次调用回调函数,其他对象将根据键值对调用回调函数。

_.each = _.forEach = function(obj, iteratee, context) {
  iteratee = optimizeCb(iteratee, context);  // 先处理一下传入的迭代函数,回顾一下,这里如果没有context,则直接使用iteratee作为函数遍历,否则迭代函数将以当前值、当前索引、完整集合作为参数进行调用
  var i, length;

  if (isArrayLike(obj)) {  // 如果是类数组对象,则遍历每一个位置
    for (i = 0, length = obj.length; i < length; i++) {
      iteratee(obj[i], i, obj);  // 参数是值、索引、完整集合
    }
  } else {  // 否则遍历每一个键值对
    var keys = _.keys(obj);
    for (i = 0, length = keys.length; i < length; i++) {
      iteratee(obj[keys[i]], keys[i], obj);  // 参数是值、键、完整集合
    }
  }
  return obj;  // 返回对象自身
};

_map、_.collect

map会对集合内的元素依次使用用户提供的回调函数进行处理,然后返回处理后的新的集合。

_.map = _.collect = function(obj, iteratee, context) {
  iteratee = cb(iteratee, context);  // 简单回顾一下,这里将根据iteratee决定是返回等价、函数调用、属性匹配或者属性访问

  var keys = !isArrayLike(obj) && _.keys(obj),  // 类数组对象为false,否则则取对象全部键
      length = (keys || obj).length,  // 类数组对象为length属性,否则为对象键值对数量
      results = Array(length);  // 要返回的新的集合

  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;  // 类数组对象取索引,否则取键名
    results[index] = iteratee(obj[currentKey], currentKey, obj);  // 放入对应位置的值经过iteratee处理后的值
  }
  return results;
};

_.reduce

递归归纳是一个非常有用的东西,通过对集合里的每个元素进行处理然后累积到一起返回。underscore对这里也进行了特殊的处理来抽象该过程,并实现左递归和右递归。

// 抽象递归过程
var createReduce = function(dir) {
  // 包装递归
  var reducer = function(obj, iteratee, memo, initial) {
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        index = dir > 0 ? 0 : length - 1;  // dir为1从左往右,为-1从右往左
    if (!initial) {  // 第一次的时候创建memo用来存储
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    // 根据方向递归遍历
    for (; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      memo = iteratee(memo, obj[currentKey], currentKey, obj);
    }
    return memo;
  };
  
  // 传入要遍历的对象、迭代器、记录、上下文
  return function(obj, iteratee, memo, context) {
    // 确认initial的初值
    var initial = arguments.length >= 3;
    // 返回迭代为累加器的迭代函数
    return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
  };
};

// 从左往右递归
_.reduce = _.foldl = _.inject = createReduce(1);
// 从右往左递归
_.reduceRight = _.foldr = createReduce(-1);

_.find、_.detect

用来返回第一个符合条件的值。

// 传入三个参数,分别是要查找的对象、判断条件、上下文
_.find = _.detect = function(obj, predicate, context) {
  var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey;  // 数组则查找索引,对象查找键
  var key = keyFinder(obj, predicate, context);
  if (key !== void 0 && key !== -1) return obj[key];
};
这里,笔者说一下void 0和undefined的区别,首先二者默认情况下是等价的,但是void 0所占字符数更少,且undefined在一定情况下可能会被赋为其他值。

查找索引和查找键也被进行了抽象,通过传递不同的参数来控制。

// 查找索引的抽象
var createPredicateIndexFinder = function(dir) {
  return function(array, predicate, context) {
    predicate = cb(predicate, context);  // 预测函数会进行迭代执行
    var length = getLength(array);
    var index = dir > 0 ? 0 : length - 1;  // 根据dir判断方向
    for (; index >= 0 && index < length; index += dir) {
      // 依次遍历
      if (predicate(array[index], index, array)) return index;
    }
    return -1;
  };
};

_.findIndex = createPredicateIndexFinder(1);  // 查找从左往右第一个符合的
_.findLastIndex = createPredicateIndexFinder(-1);  // 查找从右往左第一个符合的

// 查找键
_.findKey = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = _.keys(obj), key;
  for (var i = 0, length = keys.length; i < length; i++) {
    key = keys[i];
    if (predicate(obj[key], key, obj)) return key;
  }
};

_.filter、_.select

从原数组中寻找符合条件的并组成新的数组返回。

// 传递三个参数,分别是要查询的对象、判断函数、上下文
_.filter = _.select = function(obj, predicate, context) {
  var results = [];  // 要返回的新数组
  predicate = cb(predicate, context);  // 处理预测函数

  // 依次处理
  _.each(obj, function(value, index, list) {
    if (predicate(value, index, list)) results.push(value);  // 复合条件的放入数组
  });
  return results;
};

_.reject

从原数组中寻找符合条件的并组成新的数组返回。

// 就是filter函数的对判断函数取反
_.reject = function(obj, predicate, context) {
  return _.filter(obj, _.negate(cb(predicate)), context);
};

_.negate

对传入的判断函数,将其判断条件取反然后返回新的判断函数。

_.negate = function(predicate) {
  return function() {
    return !predicate.apply(this, arguments); // 对结果取反
  };
};

_.every、_.all

判断是不是所有项目都符合条件,全部符合才返回true,否则返回false。

_.every = _.all = function(obj, predicate, context) {
  predicate = cb(predicate, context); // 处理判断函数
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;

  // 依次遍历,一旦有不符合的就返回false
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    if (!predicate(obj[currentKey], currentKey, obj)) return false;
  }
  return true;
};

_.some、_.any

只要存在复合条件的项目就返回true,否则返回false。

_.some = _.any = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;

  // 依次遍历,一旦有符合的就返回true
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    if (predicate(obj[currentKey], currentKey, obj)) return true;
  }
  return false;
};

_.contains、_.includes、_.include

判断集合里是否包含某一项。

_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
  if (!isArrayLike(obj)) obj = _.values(obj);  // 如果是对象,取其键值对的值重组数组
  if (typeof fromIndex != 'number' || guard) fromIndex = 0;
  return _.indexOf(obj, item, fromIndex) >= 0;  // 判断从fromIndex开始是否存在该item
};

_.values

将对象中的键值对的值取出来组成一个新的数组。

_.values = function(obj) {
  var keys = _.keys(obj);  // 取出所有的键
  var length = keys.length;  // 取长度
  var values = Array(length);  // 创建新数组
  for (var i = 0; i < length; i++) {
    values[i] = obj[keys[i]];  // 依次放入相应的值
  }
  return values;
};

_.invoke

依次对集合内的每一项调用提供的方法,并将多余的参数作为该方法的参数使用。

// 先用restArgs进行包装,只传入了一个函数,因此将对所有的参数进行处理
// 它会返回一个新函数,传入这个新函数的参数将使用上面的旧函数进行调用
_.invoke = restArgs(function(obj, method, args) {
  var isFunc = _.isFunction(method);  // 先判断一下是不是函数
  return _.map(obj, function(value) {  // 依次调用执行
    var func = isFunc ? method : value[method];  // 如果是函数的话就调用该方法,否则调用value中的该方法
    return func == null ? func : func.apply(value, args);  // func不为null就调用该方法
  });
});

_.pluck

取集合中对象某一个键对应值的简便写法。

_.pluck = function(obj, key) {
  return _.map(obj, _.property(key));  // 依次取key对应的属性值
};

_.where

筛选复合某一条件的集合中的对象的简便写法。

_.where = function(obj, attrs) {
  return _.filter(obj, _.matcher(attrs));  // 取符合attrs的
};

_.findWhere

寻找集合中第一个符合某条件的对象的简便写法。

_.findWhere = function(obj, attrs) {
  return _.find(obj, _.matcher(attrs));
};

_.max

寻找集合中的最大值,如果集合是无法直接比较的,应当提供比较函数。

_.max = function(obj, iteratee, context) {
  var result = -Infinity, lastComputed = -Infinity,  // 先设定两个初值,一个是结果,一个是上一次的计算值
      value, computed;

  if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) { // 如果不提供迭代器,或者迭代器是数字且集合内不是对象,则直接比较
    obj = isArrayLike(obj) ? obj : _.values(obj);
    for (var i = 0, length = obj.length; i < length; i++) {
      value = obj[i];
      if (value != null && value > result) {
        result = value;
      }
    }
  } else { // 否则根据迭代器比较
    iteratee = cb(iteratee, context);
    _.each(obj, function(v, index, list) {
      computed = iteratee(v, index, list);
      if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
        result = v;
        lastComputed = computed;
      }
    });
  }
  return result;
};

_.min

寻找集合中的最小值,如果集合是无法直接比较的,应当提供比较函数。这个和上面的就是判断条件不一样,不再多说。

_.min = function(obj, iteratee, context) {
var result = Infinity, lastComputed = Infinity,
      value, computed;
  if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) {
    obj = isArrayLike(obj) ? obj : _.values(obj);
    for (var i = 0, length = obj.length; i < length; i++) {
      value = obj[i];
      if (value != null && value < result) {
        result = value;
      }
    }
  } else {
    iteratee = cb(iteratee, context);
    _.each(obj, function(v, index, list) {
      computed = iteratee(v, index, list);
      if (computed < lastComputed || computed === Infinity && result === Infinity) {
        result = v;
        lastComputed = computed;
      }
    });
  }
  return result;
};

_.random

取随机数的函数。

_.random = function(min, max) {
if (max == null) {  // 如果不传入第二个参数,则将第一个参数作为最大值,最小值为0
    max = min;
    min = 0;
  }

  // Math.random()取得0~1之间的值
  // 乘以最大最小之间的差值加1,可以获得0到差值+1之间的值,但因为用了Math.floor向下取整,所以可以获得0到差值之间的整数
  // 最后加上最小值
  return min + Math.floor(Math.random() * (max - min + 1));
};

_.sample

取样函数,随机取集合中的某些值成为新的集合。

// 如果不传n,则返回一个值,guard用来保证参数可以用map遍历
// 这是因为,map会给回调函数传入三个参数:当前值、当前索引、整个集合
// 这会导致n的值会传入索引,这与预期的不一样
_.sample = function(obj, n, guard) {

  // 如果不传入n,随机取一个索引对应的值  
  if (n == null || guard) {
    if (!isArrayLike(obj)) obj = _.values(obj);  // 如果是对象,则取值作为新数组
    return obj[_.random(obj.length - 1)];
  }

var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj); // 如果是类数组的,则做浅复制,否则取值作为新的数组
  var length = getLength(sample);  // 取数组长度
  n = Math.max(Math.min(n, length), 0);  // 最大值最大为数组长度
  var last = length - 1;

  // 从0到循环,先取当前索引到最后的一个随机值,然后与当前索引对应的值交换
  for (var index = 0; index < n; index++) {
var rand = _.random(index, last);
    var temp = sample[index];
    sample[index] = sample[rand];
    sample[rand] = temp;
  }

  // 切出n长度的数组返回
  return sample.slice(0, n);
};

_.shuffle

打乱一个集合。

_.shuffle = function(obj) {
  return _.sample(obj, Infinity);  // 通过向采样函数传入Infinity得到
};

_.sortBy

根据给定的函数,对集合进行排序。

_.sortBy = function(obj, iteratee, context) {
var index = 0;
  iteratee = cb(iteratee, context);

  // 首先,最外面用pluck包裹,最后会去value作为新的集合
  return _.pluck(_.map(obj, function(value, key, list) {
    // 对原对象进行处理,返回一个由新对象组成的集合
return {
value: value,  // 实际值
      index: index++,  // 索引
      criteria: iteratee(value, key, list)  // 要比较的值
    };
  }).sort(function(left, right) {
    // 然后,调用sort函数进行排序,首先更具比较值进行比较,一样的话按原顺序排列
var a = left.criteria;
    var b = right.criteria;
    if (a !== b) {
if (a > b || a === void 0) return 1;
      if (a < b || b === void 0) return -1;
    }
    return left.index - right.index;
  }), 'value');
};

group

抽象分组函数。

// 传入行为函数和分割
var group = function(behavior, partition) {
  
  // 返回一个正常的迭代函数,传入集合、迭代器和上下文
return function(obj, iteratee, context) {
var result = partition ? [[], []] : {};   // 根据是否有分割进行处理
    iteratee = cb(iteratee, context);
    _.each(obj, function(value, index) {
var key = iteratee(value, index, obj);
      behavior(result, value, key);  // 进行处理,传入参数为结果、当前值、当前键
    });
    return result;
  };
};

_.groupBy

根据传入的函数对集合进行分组,如果函数处理结果一致则放在同一组。

_.groupBy = group(function(result, value, key) {
  // 如果结果中存在key值而非原型链上的,就放入进去对应的数组,否则创建一个新数组
  if (_.has(result, key)) result[key].push(value); else result[key] = [value];
});

_.indexBy

根据某个唯一索引,将集合进行分组,需要注意的是,这里应当保证传入的key唯一。

_.indexBy = group(function(result, value, key) {
  result[key] = value;
});

_.partition

根据给定的条件将集合分为两个部分,通过放入0,不通过放入1。

_.partition = group(function(result, value, pass) {
  result[pass ? 0 : 1].push(value);
}, true);

_.countBy

类似于groupBy,但是这里是显示数量。

_.countBy = group(function(result, value, key) {
  if (_.has(result, key)) result[key]++; else result[key] = 1;
});

_.toArray

转数组的函数。

// 这段正则是对任意文字根据utf-16进行处理,来创建数组,这里简单的介绍一下
// 它分为3个部分
// \ud800-\udfff 是最为普通的,也就是说他们本身可以组成一个字符
// \ud800-\udbff \udc00-\udfff 是成对的代理项,二者通过处理得到
// \ud800-\udfff 单纯这个本身没有任何意义,因为是不成对的代理项
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;

_.toArray = function(obj) {
if (!obj) return []; // 如果无参数,返回空数组
  if (_.isArray(obj)) return slice.call(obj);  // 如果本身是数组,则使用slice创建一个等价的新数组

  if (_.isString(obj)) {
    // 这里为了保持成对的代理项仍然在一起
    return obj.match(reStrSymbol); // 按字符划分为新的字符串
  }
  if (isArrayLike(obj)) return _.map(obj, _.identity);  // 类数组的对象,直接返回自身构成的新数组
  return _.values(obj);  // 对象,将其值组成新数组
};


_.size

返回集合中的元素数量。

_.size = function(obj) {
  if (obj == null) return 0;  // 如果未传参,直接返回0
  return isArrayLike(obj) ? obj.length : _.keys(obj).length;  // 如果类数组对象,返回length值,否则返回键的数量
};
编辑于 2016-11-07 20:32