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); // 对象,将其值组成新数组
};
// 这段正则是对任意文字根据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值,否则返回键的数量
};