Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从underscore源码看如何实现map函数(一) #13

Open
webproblem opened this issue Nov 27, 2018 · 0 comments
Open

从underscore源码看如何实现map函数(一) #13

webproblem opened this issue Nov 27, 2018 · 0 comments

Comments

@webproblem
Copy link
Owner

webproblem commented Nov 27, 2018

前言

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

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

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant