lessfish / underscore-analysis

【NO LONGER UPDATE】underscore-1.8.3.js 源码解读 & 系列文章(完)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Array Functions 相关源码拾遗 & 小结

lessfish opened this issue · comments

Why underscore

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

Main

很快,Array Functions 部分到了尾声,今天来做个了(xiao)结。

underscore 给数组(以及 arguments,这里特别说明下,underscore 的数组扩展方法,同样适用于 arguments)增加了 20 个扩展方法,值得一提的是,很多有意思的方法,比如 map,shuffle 等,都被放在了 Collection Functions 中。本文来看看 Array Functions 中还有哪些有意思的方法(之前没有被提及)。

_.compact

这个方法很有意思,它的作用是剔除数组中的假值,返回数组副本。

实现非常的简单:

_.compact = function(array) {
  return _.filter(array, _.identity);
};

_.filter 我们在以后会讲到,这里你可以把它理解为 Array.prototype.filter 的一个 polyfill,来看看 _.identity 是个什么东东。

_.identity = function(value) {
  return value;
};

乍一看,_.identity 似乎没什么卵用,传入一个参数,原封不动返回这个参数,什么鬼?而再看 _.compact 的实现,就会发现非常巧妙!细细品味下,直接过滤了数组的假值,而 _.identity 在源码中能在多个地方复用。

从这个方法可以想到 PHP 的 array_filter 函数。array_filter 的基本用法和 Array.prototype.filter 相似,都是为了过滤数组中的元素。

function isOdd($num) {
  return $num & 1;
}

$a = Array(1, 2, 3);

$a = array_filter($a, 'isOdd');

var_dump($a);

// array
//   0 => int 1
//   2 => int 3

但是,值得注意的是:

If no callback is supplied, all entries of array equal to FALSE (see converting to boolean) will be removed.

这就有点 6 了,直接把 _.filter 和 _.compact 两个方法合二为一了。

$a = Array(0, 1, 2, 3, null, false, 4);

$a = array_filter($a);

var_dump($a);

// array
//   1 => int 1
//   2 => int 2
//   3 => int 3
//   6 => int 4

Array.prototype.filter 为何不设计成这样呢?没有 callback 传入的时候,直接过滤假值...

_.difference & _.without

先来看 _.without,它的作用是从数组中剔除指定的元素。

var a = [1, 2, 3, 4, 5];
var ans = _.without(a, 1, 2, 3);
console.log(ans); // [4, 5]

恩,没错,剔除数组 a 中的 value 为 1, 2, 3 的元素,这个过程中用 === 来进行比较。该方法传入的第一个参数是数组,后面的参数为单个元素。

而 _.difference 呢?和 _.without 的唯一区别是,第二个参数开始传入的是数组。(分别和数组中的元素比较)

var a = [1, 2, 3, 4, 5];
var ans = _.difference(a, [1, 2, 3], [5, 6]);
console.log(ans); // [4]

从 a 数组中剔除 1,2,3,5,6。

仔细一想,如果已经实现了 _.difference,我们把 _.without 的参数放入数组,然后传入 _.difference 就 ok 了!倒过来就不行了(思考下为什么)。 倒过来也是可以实现的,不过会相对比较繁琐些,详见 comments 部分的讨论,感谢 WangBoxue 同学指出~

来看 _.difference 的实现,非常简单:

// _.difference(array, *others)
_.difference = function(array) {
  // 将 others 数组展开一层
  // rest[] 保存展开后的元素组成的数组
  // strict 参数为 true
  // 不可以这样用 _.difference([1, 2, 3, 4, 5], [5, 2], 10);
  // 10 就会取不到
  var rest = flatten(arguments, true, true, 1);

  // 遍历 array,过滤
  return _.filter(array, function(value){
    // 如果 value 存在在 rest 中,则过滤掉
    return !_.contains(rest, value);
  });
};

不熟悉 flatten 的可以看看 前文,当 shallow 和 strict 均为 true 时,展开一层,并且过滤非数组元素,即可以起到将多个数组合并的作用。之后利用 ._filter 进行过滤即可。

而 _.without 方法则建立在 _.difference 基础上。

_.without = function(array) {
  // slice.call(arguments, 1)
  // 将 arguments 转为数组(同时去掉第一个元素)
  // 之后便可以调用 _.difference 方法
  return _.difference(array, slice.call(arguments, 1));
};

总结

数组的扩展方法就解读到这里了,相关源码可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L450-L693 这部分。接下去要解读的是 Collection Functions 部分,所谓 Collection,正是 Object & Array,也就是说这部分方法既可以用于 Object 也能用于 Array,比如我们熟悉的 map,filter,shuffle 等等,都在这部分内。

放个预告,下一篇会暂缓下 Collection Functions,讲下 array-like 相关的东西,敬请期待。

PS:坚持一件事真的挺难,一个月来,每天坚持看点源码,几乎把所有业余时间花在了上面,写了 10 篇随笔,每篇文章写的时间不短,关键还需要构思,如何提炼出一个主题,如何写让人看了会有所收获,恩,继续坚持。请关注我的 Repo https://github.com/hanzichi/underscore-analysis 支持我~

仔细一想,如果已经实现了 _.difference,我们把 _.without 的参数放入数组,然后传入 _.difference 就 ok 了!倒过来就不行了(思考下为什么)。

感觉倒过来也可以

var args = _.union(arg1, arg2, ...);
args.unshift(array);
_.without.apply(null, args);

我哪里错了吗?

_.without.apply(null, args);

这步的结果还是 args 吧?不知道我有没有理解错 @WangBoxue

@hanzichi
https://jsfiddle.net/WangBoxue/1z03b4h4/

apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。
所以执行_.without.apply(null, args)相当于_.without(array, elements in arg1, elements in arg2, ...)

@WangBoxue 的确如此,感谢指出!

commented

@WangBoxue @hanzichi

反过来实现可能需要通知 _.without() 是否是 _.difference() 来调用它。如果是,则展开参数中的所有数组,不然则不展开。当然,前提是 _.difference() 要保证调用 _.without() 时没有传递非数组元素。

_.without = function(array) {
    var rest = (this == 'difference') ? _.flatten(arguments, false, false, 1) : Array.prototype.slice.call(arugments, 1);
    
    return _.filter(array, function(value) {
        return !_.contains(rest, value);
    });
};

_.difference = function(array) {
    /** filter out arguments which is not an array firstly */
    var args = _.flatten(arguments, true, true, 1).unshift(array);

    /**
     * let the method `_.without()` know that whether it's called by `_.difference()`,
     * if so, then flatten all the arrays from argments,
     * if not, then do not flatten.
     */
    return _.without.apply('difference', args);
};

hey I have another question if you dont mind?

So in _.sample function, what is the point of having a Math.max(0,n) passed in _.shuffle function, instead of passing just 'n' ? also why does it check for object before using shuffle? because shuffle function already checks for objects? it that redundant ? please let me know what you think?

also, why does _.map function use cb(iteratee, context), instead of optimizeCb(iteratee, context) ? you cannot pass anything else but a function in _.map right? so it should be optimizeCb I think, i tried to pass other stuff but it kind of broke the _.map code. Please let me kow what you think? thanks a ton!

@shaunzeng Math.max 是过滤负数= =