前言
经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。
PS: 关于 underscore 源码解读注释,详见:underscore 源码解读。
Array.prototype.map
先来了解下原生 map 函数。
map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影响原函数的值。map 函数接受一个 callback 函数以及执行上下文参数,callback 函数带有三个参数,分别是迭代的当前值,迭代当前值的索引下标以及迭代数组自身。map 函数会给数组中的每一个元素按照顺序执行一次 callback 函数。
var arr = [1,2,3];
var newArr = arr.map(function(item, index){
if(index == 1) return item * 3;
return item;
})
console.log(newArr); // [1, 6, 3]
实现
for 循环
实现思路其实挺简单,使用 for 循环对原数组进行遍历,每个元素都执行一遍回调函数,同时将值赋值给一个新数组,遍历结束将新数组返回。
将自定义的 _map 函数依附在 Array 的原型上,省去了对迭代数组类型的检查等步骤。
Array.prototype._map = function(iteratee, context) {
var arr = this;
var newArr = [];
for(var i=0; i<arr.length; i++) {
newArr[i] = iteratee.call(context, arr[i], i, arr);
}
return newArr;
}
测试如下:
var arr = [1,2,3];
var newArr = arr._map(function(item, index){
if(index == 1) return item * 3;
return item;
})
console.log(newArr); // [1, 6, 3]
好吧,其实重点不在于自己如何实现 map 函数,而是解读 underscore 中是如何实现 map 函数的。
underscore 中的 map 函数
.map 相对于 Array.prototype.map 来说,功能更加完善和健壮。 .map 源码:
/**
* @param obj 对象
* @param iteratee 迭代回调
* @param context 执行上下文
* _.map 的强大之处在于 iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参
* _.map 会根据不同类型的 iteratee 参数进行不同的处理
* _.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]
* _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ["Kevin", "Daisy"]
*/
_.map = _.collect = function(obj, iteratee, context) {
// 针对不同类型的 iteratee 进行处理
iteratee = cb(iteratee, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
可以看到,_.map 接受 3 个参数,分别是迭代对象,迭代回调和执行上下文。iteratee 迭代回调在函数内部进行了特殊处理,为什么要这么做,原因是因为iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参。
// 传入一个函数
_.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]
// 什么也不传
_.map([1,2,3]); // [1, 2, 3]
// 传入一个对象
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]
// 传入一个字符串
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name'); // ["Kevin", "Daisy"]
先来分析下 _.map 函数内部是如何针对不同类型的 iteratee 进行处理的。
cb
cb 函数源码如下(PS: 所有的注释都是个人见解):
var cb = function(value, context, argCount) {
// 是否使用自定义的 iteratee 迭代器,外部可以自定义 iteratee 迭代器
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
// 处理不传入 iteratee 迭代器的情况,直接返回迭代集合
// _.map([1,2,3]); // [1,2,3]
if (value == null) return _.identity;
// 优化 iteratee 迭代器是函数的情况
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
// 处理 iteratee 迭代器是对象的情况
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
// 其他情况的处理,数组或者基本数据类型的情况
return _.property(value);
};
cb 函数内部针对 value 类型(也就是 iteratee 迭代器)的不同做了相应的处理。
underscore 中允许我们自定义 _.iteratee 函数的,也就是可以自定义迭代回调。
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
正常情况下,这个判断语句应该为 false,因为在 underscore 内部中已经定义了 _.iteratee 就是与 builtinIteratee 相等。
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};
这样做的目的是为了区分是否有自定义 .iteratee 函数,如果有重写了 .iteratee 函数,就使用自定义的函数。
那么为什么会允许我们去修改 .iteratee 函数呢?试想如果场景中只是需要 .map 函数的 iteratee 参数是函数的话,就用该函数处理数组元素,如果不是函数,就直接返回当前元素,而不是将 iteratee 进行针对性处理。
_.iteratee = function(value, context) {
if(typeof value === 'function') {
return function(...rest) {
return value.call(context, ...rest)
};
}
return function(value) {
return value;
}
}
测试如下:
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name');
image需要注意的是,很多迭代函数都依赖于 .iteratee 函数,所以要谨慎使用自定义.iteratee。
当然了,如果没有 iteratee 迭代器的情况下,也是直接返回迭代集合。
正常使用情况下,传入的 iteratee 迭代器应该都会是函数的,为了提升性能,在 cb 函数内部针对 iteratee 迭代器是函数的情况做了性能处理,也就是 optimizeCb 函数。
optimizeCb
optimizeCb 函数源码如下:
/**
* 优化迭代器回调
* @param func 迭代器回调
* @param context 执行上下文
* @param argCount 指定迭代器回调接受参数个数
*/
var optimizeCb = function(func, context, argCount) {
// 如果没有传入上下文,直接返回
if (context === void 0) return func;
// 根据指定接受参数进行处理
switch (argCount) {
case 1: return function(value) {
// value: 当前迭代元素
return func.call(context, value);
};
// The 2-parameter case has been omitted only because no current consumers
// made use of it.
case null:
case 3: return function(value, index, collection) {
// value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
// accumulator: 累加器,value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合
return func.call(context, accumulator, value, index, collection);
};
}
// 当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替
// 为什么不直接使用这段代码而是在上面根据 argCount 处理接受的参数
// 1. arguments 存在性能问题
// 2. call 比 apply 速度更快
return function() {
return func.apply(context, arguments);
};
};
optimizeCb 函数内部主要是针对 iteratee 迭代器接受的参数进行性能优化。当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替。为什么要这样处理?原因是因为 arguments 存在性能问题,且 call 比 apply 速度更快。具体分析会在下一篇给出解释,这里不做过多的分析。
_.matcher
回到前面对 iteratee 迭代器类型做处理的话题,如果 iteratee 迭代器是对象的情况,又该如何处理?也就是这样:
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]
在 cb 函数内部使用了 .matcher 函数处理这种情况,来分析下 .matcher 函数都做了哪些事情。 _.matcher 源码如下:
/**
* 传入一个属性对象,返回一个属性检测函数,检测对象是否具有指定属性
* var matcher = _.matcher({name: '白展堂'});
var obj = {name: '白展堂', age: 25};
matcher(obj); // true
*/
_.matcher = _.matches = function(attrs) {
// 合并复制对象,attrs 必须是 Objdect 类型
// arrts 的值为空或者其他数据类型,都能保证 attrs 是 Object 类型
attrs = _.extendOwn({}, attrs);
// 返回属性检测函数
return function(obj) {
// 检测 obj 对象是否具有指定属性 attrs
return _.isMatch(obj, attrs);
};
};
_.matcher 的主要作用就是检测 obj 对象是否具有指定属性 attrs,例如:
var matcher = _.matcher({name: '白展堂'});
var obj = {name: '白展堂', age: 25};
var obj2 = {name: '吕秀才', age: 25};
matcher(obj); // true
matcher(obj2); // false
具体的检测是使用了 .isMatch 函数, .isMatch 源码如下:
/**
* 检测对象中是否包含指定属性
* var obj = {name: '白展堂', age: 25};
* var attrs = {name: '白展堂'};
* _.isMatch(obj, attrs); // true
*/
_.isMatch = function(object, attrs) {
var keys = _.keys(attrs), length = keys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
核心部分就梳理清楚了,回到 .map 函数,可以看到,也是使用了 for 循环来实现 map 功能,和我们自己实现了思路一致,有一点不同的是, .map 函数的第一个参数,不仅限于数组,还可以是对象和字符串。
_.map('name'); // ["n", "a", "m", "e"]
_.map({name: '白展堂', age: 25}); // ["白展堂", 25]
在 _.map 函数内部,对类数组的对象也进行了处理。
遗留问题
到这里就梳理清楚了在 underscore 中是如何实现 map 函数的,以及优化性能方案。可以说在 underscore 中每行代码都很精炼,值得反复揣摩。
同时在梳理过程中,遗留了两个问题:
arguments 存在性能问题
call 比 apply 速度更快
共同学习,写下你的评论
评论加载中...
作者其他优质文章