从underscore源码看如何实现map函数

简介: 经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。

640.jpg

前言


经常会看到这样的面试题,让面试者手动实现一个 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');


640 (1).jpg


需要注意的是,很多迭代函数都依赖于 .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 速度更快


这两个问题将会在下一篇中进行详细的分析。


参考



目录
相关文章
|
6月前
|
存储 索引 容器
数据结构之Map/Set讲解+硬核源码剖析(二)
数据结构之Map/Set讲解+硬核源码剖析(二)
73 0
|
6月前
Google Earth Engine(GEE)——从列表中少选所需要的数字不用map函数,还能如何实现?简单方法介绍
Google Earth Engine(GEE)——从列表中少选所需要的数字不用map函数,还能如何实现?简单方法介绍
44 0
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
5月前
|
Python
高阶函数如`map`, `filter`, `reduce`和`functools.partial`在Python中用于函数操作
【6月更文挑战第20天】高阶函数如`map`, `filter`, `reduce`和`functools.partial`在Python中用于函数操作。装饰器如`@timer`接收或返回函数,用于扩展功能,如记录执行时间。`timer`装饰器通过包裹函数并计算执行间隙展示时间消耗,如`my_function(2)`执行耗时2秒。
34 3
|
2月前
|
数据处理 Python
Pandas中的map函数应用
Pandas中的map函数应用
19 2
|
1月前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
34 0
WK
|
2月前
|
Python
map函数
在Python中,`map()` 是一个内置的高阶函数,接受一个函数和一个或多个可迭代对象作为参数,将指定函数应用于每个元素,并返回包含应用结果的迭代器。若有多个可迭代对象,其元素会并行地传递给函数。`map()` 返回一个迭代器,需用 `list()` 转换。在Python 3中,`map()` 返回迭代器而非列表,并支持 `lambda` 表达式,适用于多种应用场景。注意,当输入的可迭代对象长度不同时,结果仅包含最短对象的长度。
WK
31 1
域对象共享数据model、modelAndView、map、mapModel、request。从源码角度分析
这篇文章详细解释了在IntelliJ IDEA中如何使用Mute Breakpoints功能来快速跳过程序中的后续断点,并展示了如何一键清空所有设置的断点。
域对象共享数据model、modelAndView、map、mapModel、request。从源码角度分析
|
5月前
|
JavaScript 前端开发
JavaScript 数组的函数 map/forEach/reduce/filter
JavaScript 数组的函数 map/forEach/reduce/filter